cocos基础工程
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
[InternetShortcut]
|
||||||
|
URL=https://docs.cocos.com/creator/manual/en/scripting/setup.html#custom-script-template
|
||||||
5
client-cocos/.creator/default-meta.json
Normal file
5
client-cocos/.creator/default-meta.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"image": {
|
||||||
|
"type": "sprite-frame"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
client-cocos/.gitignore
vendored
Normal file
24
client-cocos/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
#///////////////////////////
|
||||||
|
# Cocos Creator 3D Project
|
||||||
|
#///////////////////////////
|
||||||
|
library/
|
||||||
|
temp/
|
||||||
|
local/
|
||||||
|
build/
|
||||||
|
profiles/
|
||||||
|
native
|
||||||
|
#//////////////////////////
|
||||||
|
# NPM
|
||||||
|
#//////////////////////////
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
#//////////////////////////
|
||||||
|
# VSCode
|
||||||
|
#//////////////////////////
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
#//////////////////////////
|
||||||
|
# WebStorm
|
||||||
|
#//////////////////////////
|
||||||
|
.idea/
|
||||||
94
client-cocos/BUILD_PHYSICS_GUIDE.md
Normal file
94
client-cocos/BUILD_PHYSICS_GUIDE.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Pinball Physics 构建指南
|
||||||
|
|
||||||
|
这个脚本用于构建物理引擎并生成客户端所需的 WASM 和 asm.js 文件。
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
```batch
|
||||||
|
# 在 client-cocos 目录下运行
|
||||||
|
|
||||||
|
# 构建 debug 版本(默认)
|
||||||
|
build-physics.bat
|
||||||
|
|
||||||
|
# 或显式指定 debug
|
||||||
|
build-physics.bat debug
|
||||||
|
|
||||||
|
# 构建 release 版本(推荐用于生产环境)
|
||||||
|
build-physics.bat release
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建流程
|
||||||
|
|
||||||
|
1. **编译物理引擎**
|
||||||
|
- 进入 `../pinball-physics` 目录
|
||||||
|
- 执行 `cargo build --target wasm32-unknown-unknown [--release]`
|
||||||
|
|
||||||
|
2. **拷贝 WASM 文件**
|
||||||
|
- 源文件: `../pinball-physics/target/wasm32-unknown-unknown/[模式]/pinball_physics.wasm`
|
||||||
|
- 目标文件: `assets/wasm/pinball_physics.bin`
|
||||||
|
|
||||||
|
3. **生成 asm.js**
|
||||||
|
- 使用 `../tools/emscripten/gen-asm.bat` 工具
|
||||||
|
- 输出: `assets/wasm/pinball_physics.js`
|
||||||
|
|
||||||
|
## 输出文件
|
||||||
|
|
||||||
|
构建完成后,会在 `assets/wasm/` 目录下生成:
|
||||||
|
|
||||||
|
- **pinball_physics.bin** - WebAssembly 二进制文件
|
||||||
|
- **pinball_physics.js** - asm.js 文件(用于兼容性)
|
||||||
|
|
||||||
|
## 前置要求
|
||||||
|
|
||||||
|
1. **Rust 环境**
|
||||||
|
```batch
|
||||||
|
# 安装 Rust
|
||||||
|
# 访问 https://rustup.rs/
|
||||||
|
|
||||||
|
# 添加 WebAssembly 目标
|
||||||
|
rustup target add wasm32-unknown-unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Emscripten 环境**
|
||||||
|
```batch
|
||||||
|
# 安装 Emscripten
|
||||||
|
tools\emscripten\install.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见错误
|
||||||
|
|
||||||
|
1. **未找到 Rust 编译器**
|
||||||
|
- 确保已安装 Rust
|
||||||
|
- 检查 PATH 环境变量
|
||||||
|
|
||||||
|
2. **未找到物理引擎项目**
|
||||||
|
- 确保在正确的目录运行脚本
|
||||||
|
- 检查 `../pinball-physics/Cargo.toml` 是否存在
|
||||||
|
|
||||||
|
3. **Emscripten 转换失败**
|
||||||
|
- 确保已运行 `tools\emscripten\install.bat`
|
||||||
|
- 检查 Emscripten 环境是否正确设置
|
||||||
|
|
||||||
|
4. **权限错误**
|
||||||
|
- 确保对输出目录有写权限
|
||||||
|
- 尝试以管理员身份运行
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
|
||||||
|
- 查看详细的错误信息和文件路径
|
||||||
|
- 检查各个步骤的输出文件是否正确生成
|
||||||
|
- 验证文件大小是否合理
|
||||||
|
|
||||||
|
## 使用建议
|
||||||
|
|
||||||
|
1. **开发阶段**: 使用 `debug` 模式,编译速度更快
|
||||||
|
2. **生产环境**: 使用 `release` 模式,性能更好,文件更小
|
||||||
|
3. **兼容性**: asm.js 文件主要用于不支持 WebAssembly 的老旧浏览器
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- `../pinball-physics/` - 物理引擎 Rust 项目
|
||||||
|
- `../tools/emscripten/` - Emscripten 工具集
|
||||||
|
- `assets/wasm/` - 输出目录
|
||||||
9
client-cocos/assets/res.meta
Normal file
9
client-cocos/assets/res.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "8435224a-7b06-4067-8889-ef1344d30105",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
BIN
client-cocos/assets/res/Box.prefab
(Stored with Git LFS)
Normal file
BIN
client-cocos/assets/res/Box.prefab
(Stored with Git LFS)
Normal file
Binary file not shown.
13
client-cocos/assets/res/Box.prefab.meta
Normal file
13
client-cocos/assets/res/Box.prefab.meta
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.1.49",
|
||||||
|
"importer": "prefab",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "eb6934d9-5e1d-45c5-92f1-2b10e983ed24",
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {
|
||||||
|
"syncNodeName": "Box"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
client-cocos/assets/res/Circle.prefab
(Stored with Git LFS)
Normal file
BIN
client-cocos/assets/res/Circle.prefab
(Stored with Git LFS)
Normal file
Binary file not shown.
13
client-cocos/assets/res/Circle.prefab.meta
Normal file
13
client-cocos/assets/res/Circle.prefab.meta
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.1.49",
|
||||||
|
"importer": "prefab",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "28d6731f-90bb-49d6-a041-f8648d28269c",
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {
|
||||||
|
"syncNodeName": "Circle"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
client-cocos/assets/res/Table.prefab
(Stored with Git LFS)
Normal file
BIN
client-cocos/assets/res/Table.prefab
(Stored with Git LFS)
Normal file
Binary file not shown.
13
client-cocos/assets/res/Table.prefab.meta
Normal file
13
client-cocos/assets/res/Table.prefab.meta
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.1.49",
|
||||||
|
"importer": "prefab",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "70ac2e4f-8335-49ee-8f56-d7e5478988b8",
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {
|
||||||
|
"syncNodeName": "Table"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
client-cocos/assets/scenes.meta
Normal file
9
client-cocos/assets/scenes.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "d93688f7-8f37-4210-b0cf-5e14068a2856",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
BIN
client-cocos/assets/scenes/client-multiplayer.scene
(Stored with Git LFS)
Normal file
BIN
client-cocos/assets/scenes/client-multiplayer.scene
(Stored with Git LFS)
Normal file
Binary file not shown.
11
client-cocos/assets/scenes/client-multiplayer.scene.meta
Normal file
11
client-cocos/assets/scenes/client-multiplayer.scene.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.1.49",
|
||||||
|
"importer": "scene",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "104ca2d7-7d95-41ab-a6a9-9218131c1c0e",
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
BIN
client-cocos/assets/scenes/client-standalone.scene
(Stored with Git LFS)
Normal file
BIN
client-cocos/assets/scenes/client-standalone.scene
(Stored with Git LFS)
Normal file
Binary file not shown.
11
client-cocos/assets/scenes/client-standalone.scene.meta
Normal file
11
client-cocos/assets/scenes/client-standalone.scene.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.1.49",
|
||||||
|
"importer": "scene",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "d40eeb4c-286e-45ba-8c1e-b566914a4c0f",
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
BIN
client-cocos/assets/scenes/server-multiplayer.scene
(Stored with Git LFS)
Normal file
BIN
client-cocos/assets/scenes/server-multiplayer.scene
(Stored with Git LFS)
Normal file
Binary file not shown.
11
client-cocos/assets/scenes/server-multiplayer.scene.meta
Normal file
11
client-cocos/assets/scenes/server-multiplayer.scene.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.1.49",
|
||||||
|
"importer": "scene",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "7b9f26e0-5d5a-4124-bc21-087790bc8aac",
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
9
client-cocos/assets/scripts.meta
Normal file
9
client-cocos/assets/scripts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "67d29da8-68a1-4b50-9e7c-e2883d977ac6",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
9
client-cocos/assets/scripts/App.meta
Normal file
9
client-cocos/assets/scripts/App.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "bab444c8-af0d-4a8b-ac9b-967b84f9f626",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
81
client-cocos/assets/scripts/App/BasicGeometry.ts
Normal file
81
client-cocos/assets/scripts/App/BasicGeometry.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { _decorator, Color, Component, Node, Size, Sprite, UITransform } from 'cc';
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
@ccclass('BasicGeometry')
|
||||||
|
export class BasicGeometry extends Component {
|
||||||
|
private spriteNode: Node = null;
|
||||||
|
private spriteComponent: Sprite = null;
|
||||||
|
private uiTransform: UITransform = null;
|
||||||
|
|
||||||
|
start() {
|
||||||
|
// 获取名为"Sprite"的子节点
|
||||||
|
this.spriteNode = this.node.getChildByName("Sprite");
|
||||||
|
if (this.spriteNode) {
|
||||||
|
this.spriteComponent = this.spriteNode.getComponent(Sprite);
|
||||||
|
this.uiTransform = this.spriteNode.getComponent(UITransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.spriteNode || !this.spriteComponent) {
|
||||||
|
console.warn("BasicGeometry: 未找到名为'Sprite'的子节点或Sprite组件");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.uiTransform) {
|
||||||
|
console.warn("BasicGeometry: 未找到UITransform组件");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime: number) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置精灵的大小
|
||||||
|
* @param width 宽度
|
||||||
|
* @param height 高度
|
||||||
|
*/
|
||||||
|
public SetSize(width: number, height: number): void {
|
||||||
|
if (!this.uiTransform) {
|
||||||
|
console.warn("BasicGeometry: UITransform组件不存在,无法设置大小");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uiTransform.setContentSize(new Size(width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置精灵的颜色
|
||||||
|
* @param color 颜色值
|
||||||
|
*/
|
||||||
|
public SetColor(color: Color): void {
|
||||||
|
if (!this.spriteComponent) {
|
||||||
|
console.warn("BasicGeometry: Sprite组件不存在,无法设置颜色");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.spriteComponent.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前精灵的大小
|
||||||
|
* @returns 返回当前大小,如果组件不存在则返回null
|
||||||
|
*/
|
||||||
|
public GetSize(): Size | null {
|
||||||
|
if (!this.uiTransform) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.uiTransform.contentSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前精灵的颜色
|
||||||
|
* @returns 返回当前颜色,如果组件不存在则返回null
|
||||||
|
*/
|
||||||
|
public GetColor(): Color | null {
|
||||||
|
if (!this.spriteComponent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.spriteComponent.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
9
client-cocos/assets/scripts/App/BasicGeometry.ts.meta
Normal file
9
client-cocos/assets/scripts/App/BasicGeometry.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "257b98d4-8ead-4135-bf08-c0b7e099b1b5",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
218
client-cocos/assets/scripts/App/ClientRunner.ts
Normal file
218
client-cocos/assets/scripts/App/ClientRunner.ts
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import { _decorator, Component, Enum, Node } from 'cc';
|
||||||
|
import { PinballBootConfig, PinballBootMode, PinballBootResult, PinballBootstrap } from '../Modules/Pinball/Boot';
|
||||||
|
import { PinballManager } from '../Modules/Pinball/PinballManager';
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
// 定义模式枚举
|
||||||
|
export enum RunMode {
|
||||||
|
STANDALONE = 0,
|
||||||
|
CLIENT_MULTIPLAYER = 1,
|
||||||
|
SERVER_MULTIPLAYER = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将枚举注册到Cocos Creator
|
||||||
|
Enum(RunMode);
|
||||||
|
|
||||||
|
// RunMode 到字符串的映射
|
||||||
|
const RunModeStrings = {
|
||||||
|
[RunMode.STANDALONE]: 'standalone',
|
||||||
|
[RunMode.CLIENT_MULTIPLAYER]: 'client-multiplayer',
|
||||||
|
[RunMode.SERVER_MULTIPLAYER]: 'server-multiplayer'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// RunMode 到 PinballBootMode 的映射
|
||||||
|
const RunModeToPinballBootMode = {
|
||||||
|
[RunMode.STANDALONE]: PinballBootMode.STANDALONE,
|
||||||
|
[RunMode.CLIENT_MULTIPLAYER]: PinballBootMode.CLIENT_MULTIPLAYER,
|
||||||
|
[RunMode.SERVER_MULTIPLAYER]: PinballBootMode.SERVER_MULTIPLAYER
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@ccclass('ClientRunner')
|
||||||
|
export class ClientRunner extends Component {
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Enum(RunMode),
|
||||||
|
displayName: "运行模式",
|
||||||
|
tooltip: '选择运行模式:单机/客户端多人/服务器多人'
|
||||||
|
})
|
||||||
|
public mode: RunMode = RunMode.STANDALONE;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Node,
|
||||||
|
displayName: "主相机节点",
|
||||||
|
tooltip: '主相机节点,用于渲染游戏画面'
|
||||||
|
})
|
||||||
|
public cameraNode: Node = null;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Node,
|
||||||
|
displayName: "渲染容器",
|
||||||
|
tooltip: '渲染容器节点,所有游戏对象将在此节点下渲染'
|
||||||
|
})
|
||||||
|
public renderContainer: Node = null;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Node,
|
||||||
|
displayName: "UI容器",
|
||||||
|
tooltip: 'UI容器节点,用于显示游戏UI界面'
|
||||||
|
})
|
||||||
|
public uiContainer: Node = null;
|
||||||
|
|
||||||
|
private pinballManager: PinballManager = null;
|
||||||
|
private bootResult: PinballBootResult = null;
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
console.log(`[ClientRunner] 开始启动,运行模式: ${this.getModeString()}`);
|
||||||
|
|
||||||
|
// 使用 Bootstrap 启动游戏
|
||||||
|
await this.bootWithBootstrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 Bootstrap 启动游戏
|
||||||
|
*/
|
||||||
|
private async bootWithBootstrap(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 创建启动配置
|
||||||
|
const config = this.createBootConfig();
|
||||||
|
|
||||||
|
// 使用 Bootstrap 启动
|
||||||
|
const bootstrap = PinballBootstrap.getInstance();
|
||||||
|
this.bootResult = await bootstrap.boot(this, config);
|
||||||
|
|
||||||
|
if (this.bootResult.success) {
|
||||||
|
this.pinballManager = this.bootResult.pinballManager;
|
||||||
|
console.log(`[ClientRunner] 使用 Bootstrap 启动成功: ${this.bootResult.mode}`);
|
||||||
|
} else {
|
||||||
|
console.error(`[ClientRunner] Bootstrap 启动失败: ${this.bootResult.error}`);
|
||||||
|
this.handleBootFailure(this.bootResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ClientRunner] Bootstrap 启动异常:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建启动配置
|
||||||
|
*/
|
||||||
|
private createBootConfig(): PinballBootConfig {
|
||||||
|
// 将 RunMode 转换为 PinballBootMode
|
||||||
|
const bootMode = this.convertRunModeToPinballBootMode(this.mode);
|
||||||
|
|
||||||
|
// 使用默认配置并设置节点引用
|
||||||
|
const config = PinballBootstrap.createDefaultConfig(bootMode);
|
||||||
|
config.cameraNode = this.cameraNode;
|
||||||
|
config.renderContainer = this.renderContainer;
|
||||||
|
config.uiContainer = this.uiContainer;
|
||||||
|
|
||||||
|
// 根据模式添加特定配置
|
||||||
|
if (bootMode === PinballBootMode.CLIENT_MULTIPLAYER ||
|
||||||
|
bootMode === PinballBootMode.SERVER_MULTIPLAYER) {
|
||||||
|
config.multiplayerConfig = {
|
||||||
|
serverAddress: 'localhost:3000', // 默认服务器地址
|
||||||
|
playerName: 'Player1',
|
||||||
|
roomId: 'default'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换 RunMode 到 PinballBootMode
|
||||||
|
*/
|
||||||
|
private convertRunModeToPinballBootMode(runMode: RunMode): PinballBootMode {
|
||||||
|
return RunModeToPinballBootMode[runMode] ?? PinballBootMode.STANDALONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理启动失败
|
||||||
|
*/
|
||||||
|
private handleBootFailure(result: PinballBootResult): void {
|
||||||
|
console.error(`[ClientRunner] 启动失败 - 模式: ${result.mode}, 错误: ${result.error}`);
|
||||||
|
|
||||||
|
// 可以在这里添加错误恢复逻辑,比如回退到 Standalone 模式
|
||||||
|
if (result.mode !== PinballBootMode.STANDALONE) {
|
||||||
|
console.log('[ClientRunner] 尝试回退到 Standalone 模式...');
|
||||||
|
setTimeout(async () => {
|
||||||
|
this.mode = RunMode.STANDALONE;
|
||||||
|
await this.bootWithBootstrap();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime: number) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
// 清理 PinballManager
|
||||||
|
if (this.pinballManager) {
|
||||||
|
this.pinballManager = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 PinballManager 实例
|
||||||
|
*/
|
||||||
|
public getPinballManager(): PinballManager | null {
|
||||||
|
return this.pinballManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启当前模式
|
||||||
|
*/
|
||||||
|
public async restart(): Promise<void> {
|
||||||
|
console.log('[ClientRunner] 重启游戏...');
|
||||||
|
|
||||||
|
if (this.pinballManager) {
|
||||||
|
await this.pinballManager.restartGame();
|
||||||
|
} else {
|
||||||
|
// 如果没有 PinballManager,重新执行启动流程
|
||||||
|
await this.bootWithBootstrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换模式并重启
|
||||||
|
*/
|
||||||
|
public async switchMode(newMode: RunMode): Promise<void> {
|
||||||
|
console.log(`[ClientRunner] 切换模式从 ${this.getModeString()} 到 ${this.getModeStringForMode(newMode)}`);
|
||||||
|
|
||||||
|
// 停止当前游戏
|
||||||
|
if (this.pinballManager) {
|
||||||
|
await this.pinballManager.stopCurrentGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新模式
|
||||||
|
this.mode = newMode;
|
||||||
|
|
||||||
|
// 重新启动
|
||||||
|
await this.bootWithBootstrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取启动结果
|
||||||
|
*/
|
||||||
|
public getBootResult(): PinballBootResult | null {
|
||||||
|
return this.bootResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定模式的字符串值
|
||||||
|
*/
|
||||||
|
private getModeStringForMode(mode: RunMode): string {
|
||||||
|
return RunModeStrings[mode] ?? 'standalone';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前模式的字符串值
|
||||||
|
* @returns 模式字符串
|
||||||
|
*/
|
||||||
|
public getModeString(): string {
|
||||||
|
return this.getModeStringForMode(this.mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
9
client-cocos/assets/scripts/App/ClientRunner.ts.meta
Normal file
9
client-cocos/assets/scripts/App/ClientRunner.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "594d765c-8674-48a1-a60f-06e438cdfb47",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
9
client-cocos/assets/scripts/Modules.meta
Normal file
9
client-cocos/assets/scripts/Modules.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "cd3c8566-25ad-4c45-acb5-d4622f704aa6",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
9
client-cocos/assets/scripts/Modules/Pinball.meta
Normal file
9
client-cocos/assets/scripts/Modules/Pinball.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "c4fb263a-8354-4597-9436-e72c9e9f189d",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
9
client-cocos/assets/scripts/Modules/Pinball/Boot.meta
Normal file
9
client-cocos/assets/scripts/Modules/Pinball/Boot.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "03edfca7-426e-4a79-aa6f-9bd259a9ead3",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* 启动器基类
|
||||||
|
* 定义不同启动模式的统一接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from 'cc';
|
||||||
|
import { PinballManager } from '../PinballManager';
|
||||||
|
import { PinballBootConfig } from './BootTypes';
|
||||||
|
|
||||||
|
export abstract class BaseBooter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动特定模式的游戏
|
||||||
|
* @param hostComponent 宿主组件
|
||||||
|
* @param config 启动配置
|
||||||
|
*/
|
||||||
|
abstract boot(hostComponent: Component, config: PinballBootConfig): Promise<PinballManager>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用配置到 PinballManager
|
||||||
|
* @param pinballManager PinballManager 实例
|
||||||
|
* @param config 启动配置
|
||||||
|
*/
|
||||||
|
protected applyConfiguration(pinballManager: PinballManager, config: PinballBootConfig): void {
|
||||||
|
// 设置节点引用
|
||||||
|
if (config.cameraNode) {
|
||||||
|
pinballManager.cameraNode = config.cameraNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.renderContainer) {
|
||||||
|
pinballManager.renderContainer = config.renderContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.uiContainer) {
|
||||||
|
pinballManager.uiContainer = config.uiContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置基础配置
|
||||||
|
pinballManager.autoStart = config.autoStart !== false; // 默认为 true
|
||||||
|
pinballManager.debugMode = config.debugMode || false;
|
||||||
|
|
||||||
|
// 应用自定义配置
|
||||||
|
if (config.physicsConfig || config.renderConfig || config.wasmPath) {
|
||||||
|
const pinballConfig = pinballManager.getConfig();
|
||||||
|
|
||||||
|
if (!pinballConfig) {
|
||||||
|
pinballManager.updateConfig(config);
|
||||||
|
} else {
|
||||||
|
if (config.physicsConfig) {
|
||||||
|
pinballConfig.physicsSettings = {
|
||||||
|
...pinballConfig.physicsSettings,
|
||||||
|
gravity: config.physicsConfig.gravity || { x: 0, y: -9.81 },
|
||||||
|
timeStep: config.physicsConfig.timeStep || 1 / 60
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.renderConfig) {
|
||||||
|
pinballConfig.renderSettings = {
|
||||||
|
...pinballConfig.renderSettings,
|
||||||
|
enableEffects: config.renderConfig.enableEffects !== false,
|
||||||
|
maxParticles: config.renderConfig.maxParticles || 500
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.wasmPath) {
|
||||||
|
pinballConfig.wasmPath = config.wasmPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
pinballManager.updateConfig(pinballConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 条件日志输出
|
||||||
|
*/
|
||||||
|
protected log(message: string, enabled?: boolean): void {
|
||||||
|
if (enabled !== false) {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "73bc9ff5-962b-4dd1-801f-0671a621bc9b",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Pinball 启动配置接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Node } from 'cc';
|
||||||
|
|
||||||
|
export enum PinballBootMode {
|
||||||
|
STANDALONE = 'standalone',
|
||||||
|
CLIENT_MULTIPLAYER = 'client-multiplayer',
|
||||||
|
SERVER_MULTIPLAYER = 'server-multiplayer'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PinballBootConfig {
|
||||||
|
/** 启动模式 */
|
||||||
|
mode: PinballBootMode;
|
||||||
|
|
||||||
|
/** 主相机节点 */
|
||||||
|
cameraNode?: Node;
|
||||||
|
|
||||||
|
/** 渲染容器节点 */
|
||||||
|
renderContainer?: Node;
|
||||||
|
|
||||||
|
/** UI容器节点 */
|
||||||
|
uiContainer?: Node;
|
||||||
|
|
||||||
|
/** 是否启用调试模式 */
|
||||||
|
debugMode?: boolean;
|
||||||
|
|
||||||
|
/** 是否自动启动 */
|
||||||
|
autoStart?: boolean;
|
||||||
|
|
||||||
|
/** 物理引擎配置 */
|
||||||
|
physicsConfig?: {
|
||||||
|
gravity?: { x: number; y: number };
|
||||||
|
timeStep?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 渲染配置 */
|
||||||
|
renderConfig?: {
|
||||||
|
enableEffects?: boolean;
|
||||||
|
maxParticles?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 多人模式配置 */
|
||||||
|
multiplayerConfig?: {
|
||||||
|
serverAddress?: string;
|
||||||
|
playerName?: string;
|
||||||
|
roomId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** WASM文件路径 */
|
||||||
|
wasmPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PinballBootResult {
|
||||||
|
/** 启动是否成功 */
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
/** 错误信息(如果失败) */
|
||||||
|
error?: string;
|
||||||
|
|
||||||
|
/** 启动的模式 */
|
||||||
|
mode: PinballBootMode;
|
||||||
|
|
||||||
|
/** PinballManager 实例 */
|
||||||
|
pinballManager?: any;
|
||||||
|
|
||||||
|
/** 启动时间戳 */
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "d101ed2a-e68c-4b3b-831b-04d84f0596f0",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "3efea1af-73f2-404f-a948-e059b92c65c1",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Client Multiplayer 模式启动器
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from 'cc';
|
||||||
|
import { PinballManager } from '../../PinballManager';
|
||||||
|
import { BaseBooter } from '../BaseBooter';
|
||||||
|
import { PinballBootConfig } from '../BootTypes';
|
||||||
|
|
||||||
|
export class ClientMultiplayerBooter extends BaseBooter {
|
||||||
|
|
||||||
|
async boot(hostComponent: Component, config: PinballBootConfig): Promise<PinballManager> {
|
||||||
|
this.log('[ClientMultiplayerBooter] 正在启动客户端多人模式...', config.debugMode);
|
||||||
|
|
||||||
|
// TODO: 实现客户端多人模式启动逻辑
|
||||||
|
// 1. 连接到游戏服务器
|
||||||
|
// 2. 加入游戏房间
|
||||||
|
// 3. 设置网络同步和状态管理
|
||||||
|
|
||||||
|
throw new Error('客户端多人模式尚未实现');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "57dd41d5-1108-443c-8888-9aa695b552ce",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Server Multiplayer 模式启动器
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from 'cc';
|
||||||
|
import { PinballManager } from '../../PinballManager';
|
||||||
|
import { BaseBooter } from '../BaseBooter';
|
||||||
|
import { PinballBootConfig } from '../BootTypes';
|
||||||
|
|
||||||
|
export class ServerMultiplayerBooter extends BaseBooter {
|
||||||
|
|
||||||
|
async boot(hostComponent: Component, config: PinballBootConfig): Promise<PinballManager> {
|
||||||
|
this.log('[ServerMultiplayerBooter] 正在启动服务器多人模式...', config.debugMode);
|
||||||
|
|
||||||
|
// TODO: 实现服务器多人模式启动逻辑
|
||||||
|
// 1. 连接到 SpacetimeDB
|
||||||
|
// 2. 创建房间或加入现有房间
|
||||||
|
// 3. 设置网络同步
|
||||||
|
|
||||||
|
throw new Error('服务器多人模式尚未实现');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "6f237677-8531-40fc-af0a-c1e9f020528a",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Standalone 模式启动器
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from 'cc';
|
||||||
|
import { PinballManager, PinballMode } from '../../PinballManager';
|
||||||
|
import { BaseBooter } from '../BaseBooter';
|
||||||
|
import { PinballBootConfig } from '../BootTypes';
|
||||||
|
|
||||||
|
export class StandaloneBooter extends BaseBooter {
|
||||||
|
|
||||||
|
async boot(hostComponent: Component, config: PinballBootConfig): Promise<PinballManager> {
|
||||||
|
this.log('[StandaloneBooter] 正在启动 Standalone 模式...', config.debugMode);
|
||||||
|
|
||||||
|
// 创建或获取 PinballManager 组件
|
||||||
|
let pinballManager = hostComponent.getComponent(PinballManager);
|
||||||
|
if (!pinballManager) {
|
||||||
|
pinballManager = hostComponent.addComponent(PinballManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用配置
|
||||||
|
this.applyConfiguration(pinballManager, config);
|
||||||
|
|
||||||
|
// 设置默认模式
|
||||||
|
pinballManager.defaultMode = PinballMode.STANDALONE;
|
||||||
|
|
||||||
|
// 启动 PinballManager(在配置应用后)
|
||||||
|
const startSuccess = await pinballManager.Start();
|
||||||
|
if (!startSuccess) {
|
||||||
|
throw new Error('PinballManager 启动失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动 Standalone 模式
|
||||||
|
const success = await pinballManager.startGame(PinballMode.STANDALONE);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error('Standalone 模式启动失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('[StandaloneBooter] Standalone 模式启动完成', config.debugMode);
|
||||||
|
return pinballManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "b1ed032a-5ed8-4dee-b821-8d4a4558ae00",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/**
|
||||||
|
* Pinball 启动工具类
|
||||||
|
* 提供常用的启动配置和便捷方法
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from 'cc';
|
||||||
|
import { PinballBootConfig, PinballBootMode, PinballBootResult, PinballBootstrap } from './index';
|
||||||
|
|
||||||
|
export class PinballBootUtils {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建高性能配置(适合移动设备)
|
||||||
|
*/
|
||||||
|
public static createMobileConfig(mode: PinballBootMode): PinballBootConfig {
|
||||||
|
const config = PinballBootstrap.createDefaultConfig(mode);
|
||||||
|
|
||||||
|
// 移动设备优化配置
|
||||||
|
config.renderConfig = {
|
||||||
|
enableEffects: false, // 关闭粒子效果以提升性能
|
||||||
|
maxParticles: 100 // 降低粒子数量
|
||||||
|
};
|
||||||
|
|
||||||
|
config.physicsConfig = {
|
||||||
|
gravity: { x: 0, y: -9.81 },
|
||||||
|
timeStep: 1 / 30 // 降低物理步进频率
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建高质量配置(适合桌面设备)
|
||||||
|
*/
|
||||||
|
public static createDesktopConfig(mode: PinballBootMode): PinballBootConfig {
|
||||||
|
const config = PinballBootstrap.createDefaultConfig(mode);
|
||||||
|
|
||||||
|
// 桌面设备高质量配置
|
||||||
|
config.renderConfig = {
|
||||||
|
enableEffects: true, // 启用所有粒子效果
|
||||||
|
maxParticles: 1000 // 更多粒子数量
|
||||||
|
};
|
||||||
|
|
||||||
|
config.physicsConfig = {
|
||||||
|
gravity: { x: 0, y: -9.81 },
|
||||||
|
timeStep: 1 / 120 // 更高的物理步进频率
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建调试配置
|
||||||
|
*/
|
||||||
|
public static createDebugConfig(mode: PinballBootMode): PinballBootConfig {
|
||||||
|
const config = PinballBootstrap.createDefaultConfig(mode);
|
||||||
|
config.debugMode = true;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建生产配置
|
||||||
|
*/
|
||||||
|
public static createProductionConfig(mode: PinballBootMode): PinballBootConfig {
|
||||||
|
const config = PinballBootstrap.createDefaultConfig(mode);
|
||||||
|
config.debugMode = false;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量启动多个模式(用于测试)
|
||||||
|
*/
|
||||||
|
public static async batchBoot(
|
||||||
|
hostComponent: Component,
|
||||||
|
configs: PinballBootConfig[]
|
||||||
|
): Promise<PinballBootResult[]> {
|
||||||
|
const bootstrap = PinballBootstrap.getInstance();
|
||||||
|
const results: PinballBootResult[] = [];
|
||||||
|
|
||||||
|
for (const config of configs) {
|
||||||
|
console.log(`[PinballBootUtils] 批量启动: ${config.mode}`);
|
||||||
|
const result = await bootstrap.boot(hostComponent, config);
|
||||||
|
results.push(result);
|
||||||
|
|
||||||
|
// 如果启动失败,停止批量启动
|
||||||
|
if (!result.success) {
|
||||||
|
console.error(`[PinballBootUtils] 批量启动在 ${config.mode} 模式失败: ${result.error}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性能基准测试启动
|
||||||
|
*/
|
||||||
|
public static async benchmarkBoot(
|
||||||
|
hostComponent: Component,
|
||||||
|
config: PinballBootConfig,
|
||||||
|
iterations: number = 5
|
||||||
|
): Promise<{ averageTime: number; results: PinballBootResult[] }> {
|
||||||
|
const bootstrap = PinballBootstrap.getInstance();
|
||||||
|
const results: PinballBootResult[] = [];
|
||||||
|
let totalTime = 0;
|
||||||
|
|
||||||
|
console.log(`[PinballBootUtils] 开始性能基准测试,迭代次数: ${iterations}`);
|
||||||
|
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
const startTime = performance.now();
|
||||||
|
const result = await bootstrap.boot(hostComponent, config);
|
||||||
|
const endTime = performance.now();
|
||||||
|
|
||||||
|
const duration = endTime - startTime;
|
||||||
|
totalTime += duration;
|
||||||
|
|
||||||
|
results.push(result);
|
||||||
|
|
||||||
|
console.log(`[PinballBootUtils] 第 ${i + 1} 次启动耗时: ${duration.toFixed(2)}ms`);
|
||||||
|
|
||||||
|
// 清理资源准备下一次测试
|
||||||
|
if (result.success && result.pinballManager) {
|
||||||
|
await result.pinballManager.stopCurrentGame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const averageTime = totalTime / iterations;
|
||||||
|
console.log(`[PinballBootUtils] 平均启动时间: ${averageTime.toFixed(2)}ms`);
|
||||||
|
|
||||||
|
return { averageTime, results };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证启动结果
|
||||||
|
*/
|
||||||
|
public static validateBootResult(result: PinballBootResult): {
|
||||||
|
isValid: boolean;
|
||||||
|
issues: string[];
|
||||||
|
} {
|
||||||
|
const issues: string[] = [];
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
issues.push(`启动失败: ${result.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.pinballManager) {
|
||||||
|
issues.push('PinballManager 实例不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.mode) {
|
||||||
|
issues.push('启动模式未定义');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.timestamp || result.timestamp <= 0) {
|
||||||
|
issues.push('时间戳无效');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: issues.length === 0,
|
||||||
|
issues
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成启动报告
|
||||||
|
*/
|
||||||
|
public static generateBootReport(results: PinballBootResult[]): string {
|
||||||
|
const successCount = results.filter(r => r.success).length;
|
||||||
|
const failureCount = results.length - successCount;
|
||||||
|
|
||||||
|
let report = '=== Pinball 启动报告 ===\n';
|
||||||
|
report += `总启动次数: ${results.length}\n`;
|
||||||
|
report += `成功启动: ${successCount}\n`;
|
||||||
|
report += `启动失败: ${failureCount}\n`;
|
||||||
|
report += `成功率: ${((successCount / results.length) * 100).toFixed(1)}%\n\n`;
|
||||||
|
|
||||||
|
if (failureCount > 0) {
|
||||||
|
report += '失败详情:\n';
|
||||||
|
results.filter(r => !r.success).forEach((result, index) => {
|
||||||
|
report += `${index + 1}. ${result.mode}: ${result.error}\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "e796606d-57a8-45e7-b700-66f4c28ccaed",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* Pinball 启动管理器
|
||||||
|
* 负责维护游戏启动状态和调度具体启动器
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from 'cc';
|
||||||
|
import { PinballBootConfig, PinballBootMode, PinballBootResult } from './BootTypes';
|
||||||
|
import { ClientMultiplayerBooter } from './Mode/ClientMultiplayerBooter';
|
||||||
|
import { ServerMultiplayerBooter } from './Mode/ServerMultiplayerBooter';
|
||||||
|
import { StandaloneBooter } from './Mode/StandaloneBooter';
|
||||||
|
|
||||||
|
export class PinballBootstrap {
|
||||||
|
private static instance: PinballBootstrap;
|
||||||
|
private currentResult: PinballBootResult | null = null;
|
||||||
|
|
||||||
|
/** 获取单例实例 */
|
||||||
|
public static getInstance(): PinballBootstrap {
|
||||||
|
if (!PinballBootstrap.instance) {
|
||||||
|
PinballBootstrap.instance = new PinballBootstrap();
|
||||||
|
}
|
||||||
|
return PinballBootstrap.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动 Pinball 游戏
|
||||||
|
*/
|
||||||
|
public async boot(hostComponent: Component, config: PinballBootConfig): Promise<PinballBootResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[PinballBootstrap] 开始启动 ${config.mode} 模式`);
|
||||||
|
|
||||||
|
// 获取对应的启动器
|
||||||
|
const booter = this.getBooter(config.mode);
|
||||||
|
|
||||||
|
// 执行启动
|
||||||
|
const pinballManager = await booter.boot(hostComponent, config);
|
||||||
|
|
||||||
|
// 记录启动结果
|
||||||
|
this.currentResult = {
|
||||||
|
success: true,
|
||||||
|
mode: config.mode,
|
||||||
|
pinballManager,
|
||||||
|
timestamp: startTime
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[PinballBootstrap] ${config.mode} 模式启动成功`);
|
||||||
|
return this.currentResult;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`[PinballBootstrap] 启动失败:`, error);
|
||||||
|
|
||||||
|
this.currentResult = {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
mode: config.mode,
|
||||||
|
timestamp: startTime
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.currentResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取启动器实例
|
||||||
|
*/
|
||||||
|
private getBooter(mode: PinballBootMode) {
|
||||||
|
switch (mode) {
|
||||||
|
case PinballBootMode.STANDALONE:
|
||||||
|
return new StandaloneBooter();
|
||||||
|
case PinballBootMode.CLIENT_MULTIPLAYER:
|
||||||
|
return new ClientMultiplayerBooter();
|
||||||
|
case PinballBootMode.SERVER_MULTIPLAYER:
|
||||||
|
return new ServerMultiplayerBooter();
|
||||||
|
default:
|
||||||
|
throw new Error(`不支持的启动模式: ${mode}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前启动结果
|
||||||
|
*/
|
||||||
|
public getCurrentResult(): PinballBootResult | null {
|
||||||
|
return this.currentResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除启动状态
|
||||||
|
*/
|
||||||
|
public clearState(): void {
|
||||||
|
this.currentResult = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认配置
|
||||||
|
*/
|
||||||
|
public static createDefaultConfig(mode: PinballBootMode): PinballBootConfig {
|
||||||
|
return {
|
||||||
|
mode,
|
||||||
|
debugMode: true,
|
||||||
|
autoStart: true,
|
||||||
|
physicsConfig: {
|
||||||
|
gravity: { x: 0, y: -9.81 },
|
||||||
|
timeStep: 1 / 60
|
||||||
|
},
|
||||||
|
renderConfig: {
|
||||||
|
enableEffects: true,
|
||||||
|
maxParticles: 500
|
||||||
|
},
|
||||||
|
wasmPath: 'assets/wasm/pinball_physics.wasm'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "9cdad6b2-b0b6-4223-b44f-fbdf0f37197c",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
12
client-cocos/assets/scripts/Modules/Pinball/Boot/index.ts
Normal file
12
client-cocos/assets/scripts/Modules/Pinball/Boot/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Pinball Boot 模块入口
|
||||||
|
* 导出启动相关的所有类型和类
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { PinballBootMode } from './BootTypes';
|
||||||
|
export type {
|
||||||
|
PinballBootConfig,
|
||||||
|
PinballBootResult
|
||||||
|
} from './BootTypes';
|
||||||
|
export { PinballBootstrap } from './PinballBootstrap';
|
||||||
|
export { PinballBootUtils } from './PinballBootUtils';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "618d7e5f-842b-4506-a4ad-f55fc18f102b",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
9
client-cocos/assets/scripts/Modules/Pinball/Core.meta
Normal file
9
client-cocos/assets/scripts/Modules/Pinball/Core.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "af23f3f9-fa61-4053-9a39-a4fccd83f14e",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
88
client-cocos/assets/scripts/Modules/Pinball/Core/EventBus.ts
Normal file
88
client-cocos/assets/scripts/Modules/Pinball/Core/EventBus.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* 简单的事件总线实现
|
||||||
|
* 用于 Pinball 模块内部组件间通信
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type EventCallback<T = any> = (data: T) => void;
|
||||||
|
|
||||||
|
export class EventBus {
|
||||||
|
private static instance: EventBus;
|
||||||
|
private events: Map<string, EventCallback[]> = new Map();
|
||||||
|
|
||||||
|
private constructor() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单例实例
|
||||||
|
*/
|
||||||
|
static getInstance(): EventBus {
|
||||||
|
if (!EventBus.instance) {
|
||||||
|
EventBus.instance = new EventBus();
|
||||||
|
}
|
||||||
|
return EventBus.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅事件
|
||||||
|
*/
|
||||||
|
on<T = any>(event: string, callback: EventCallback<T>): void {
|
||||||
|
if (!this.events.has(event)) {
|
||||||
|
this.events.set(event, []);
|
||||||
|
}
|
||||||
|
this.events.get(event)!.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消订阅事件
|
||||||
|
*/
|
||||||
|
off<T = any>(event: string, callback: EventCallback<T>): void {
|
||||||
|
const callbacks = this.events.get(event);
|
||||||
|
if (callbacks) {
|
||||||
|
const index = callbacks.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
callbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发射事件
|
||||||
|
*/
|
||||||
|
emit<T = any>(event: string, data?: T): void {
|
||||||
|
const callbacks = this.events.get(event);
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in event callback for event '${event}':`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只订阅一次事件
|
||||||
|
*/
|
||||||
|
once<T = any>(event: string, callback: EventCallback<T>): void {
|
||||||
|
const onceCallback: EventCallback<T> = (data: T) => {
|
||||||
|
callback(data);
|
||||||
|
this.off(event, onceCallback);
|
||||||
|
};
|
||||||
|
this.on(event, onceCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理所有事件监听器
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this.events.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件监听器数量
|
||||||
|
*/
|
||||||
|
listenerCount(event: string): number {
|
||||||
|
const callbacks = this.events.get(event);
|
||||||
|
return callbacks ? callbacks.length : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "1dc34aed-d9d7-4cd7-8bf7-5d4a1564882f",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
74
client-cocos/assets/scripts/Modules/Pinball/Core/GameData.ts
Normal file
74
client-cocos/assets/scripts/Modules/Pinball/Core/GameData.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Pinball 游戏核心数据结构定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 2D 向量 */
|
||||||
|
export interface Vector2 {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 物理体 ID */
|
||||||
|
export type BodyId = number;
|
||||||
|
|
||||||
|
/** 世界 ID */
|
||||||
|
export type WorldId = number;
|
||||||
|
|
||||||
|
/** 物理体数据 */
|
||||||
|
export interface PhysicsBodyData {
|
||||||
|
id: BodyId;
|
||||||
|
position: Vector2;
|
||||||
|
velocity: Vector2;
|
||||||
|
rotation: number;
|
||||||
|
angularVelocity: number;
|
||||||
|
bodyType: 'circle' | 'box';
|
||||||
|
radius?: number;
|
||||||
|
size?: Vector2;
|
||||||
|
isStatic: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 游戏状态 */
|
||||||
|
export interface GameState {
|
||||||
|
worldId: WorldId;
|
||||||
|
bodies: Map<BodyId, PhysicsBodyData>;
|
||||||
|
isPaused: boolean;
|
||||||
|
timeStep: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pinball 配置 */
|
||||||
|
export interface PinballConfig {
|
||||||
|
mode: 'standalone' | 'client-multiplayer' | 'server-multiplayer';
|
||||||
|
serverAddress?: string;
|
||||||
|
wasmPath?: string;
|
||||||
|
physicsSettings?: {
|
||||||
|
gravity: Vector2;
|
||||||
|
timeStep: number;
|
||||||
|
};
|
||||||
|
renderSettings?: {
|
||||||
|
enableEffects: boolean;
|
||||||
|
maxParticles: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 物理设置 */
|
||||||
|
export interface PhysicsSettings {
|
||||||
|
gravity: Vector2;
|
||||||
|
timeStep: number;
|
||||||
|
maxBodies: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 游戏事件类型 */
|
||||||
|
export enum GameEventType {
|
||||||
|
PHYSICS_STEP = 'physics_step',
|
||||||
|
BODY_CREATED = 'body_created',
|
||||||
|
BODY_DESTROYED = 'body_destroyed',
|
||||||
|
WORLD_RESET = 'world_reset',
|
||||||
|
INPUT_RECEIVED = 'input_received'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 游戏事件数据 */
|
||||||
|
export interface GameEvent {
|
||||||
|
type: GameEventType;
|
||||||
|
data?: any;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "ff4e5c1d-69eb-4234-9087-1b84b837d708",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* 物理引擎接口
|
||||||
|
* 定义了不同物理引擎实现需要遵循的契约
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BodyId, PhysicsBodyData, PhysicsSettings, Vector2, WorldId } from './GameData';
|
||||||
|
|
||||||
|
export interface CreateCircleOptions {
|
||||||
|
position: Vector2;
|
||||||
|
radius: number;
|
||||||
|
isStatic: boolean;
|
||||||
|
density?: number;
|
||||||
|
restitution?: number;
|
||||||
|
friction?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateBoxOptions {
|
||||||
|
position: Vector2;
|
||||||
|
size: Vector2;
|
||||||
|
isStatic: boolean;
|
||||||
|
density?: number;
|
||||||
|
restitution?: number;
|
||||||
|
friction?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPhysicsEngine {
|
||||||
|
/**
|
||||||
|
* 初始化物理引擎
|
||||||
|
*/
|
||||||
|
initialize(settings?: PhysicsSettings): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建物理世界
|
||||||
|
*/
|
||||||
|
createWorld(gravity: Vector2): Promise<WorldId>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁物理世界
|
||||||
|
*/
|
||||||
|
destroyWorld(worldId: WorldId): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行物理步进
|
||||||
|
*/
|
||||||
|
step(deltaTime: number, worldId?: WorldId): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建圆形刚体
|
||||||
|
*/
|
||||||
|
createCircle(options: CreateCircleOptions): BodyId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建矩形刚体
|
||||||
|
*/
|
||||||
|
createBox(options: CreateBoxOptions): BodyId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建动态刚体
|
||||||
|
*/
|
||||||
|
createDynamicBody(worldId: WorldId, position: Vector2, radius: number): Promise<BodyId>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建静态刚体
|
||||||
|
*/
|
||||||
|
createStaticBody(worldId: WorldId, position: Vector2, radius: number): Promise<BodyId>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁刚体
|
||||||
|
*/
|
||||||
|
destroyBody(worldId: WorldId, bodyId: BodyId): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除刚体
|
||||||
|
*/
|
||||||
|
removeBody(bodyId: BodyId): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取刚体位置
|
||||||
|
*/
|
||||||
|
getBodyPosition(worldId: WorldId, bodyId: BodyId): Promise<Vector2 | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置刚体位置
|
||||||
|
*/
|
||||||
|
setBodyPosition(worldId: WorldId, bodyId: BodyId, position: Vector2): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取刚体速度
|
||||||
|
*/
|
||||||
|
getBodyVelocity(worldId: WorldId, bodyId: BodyId): Promise<Vector2 | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置刚体速度
|
||||||
|
*/
|
||||||
|
setBodyVelocity(worldId: WorldId, bodyId: BodyId, velocity: Vector2): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取刚体数据
|
||||||
|
*/
|
||||||
|
getBodyData(bodyId: BodyId): PhysicsBodyData | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有物理体数据
|
||||||
|
*/
|
||||||
|
getAllBodies(worldId: WorldId): Promise<PhysicsBodyData[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源
|
||||||
|
*/
|
||||||
|
cleanup(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源 (别名)
|
||||||
|
*/
|
||||||
|
dispose(): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "b15a76bd-406d-4463-8a9c-1e82229ab995",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* 渲染器接口
|
||||||
|
* 定义了渲染系统需要实现的方法
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PhysicsBodyData, Vector2 } from './GameData';
|
||||||
|
|
||||||
|
/** 渲染对象数据 */
|
||||||
|
export interface RenderObject {
|
||||||
|
id: string;
|
||||||
|
position: Vector2;
|
||||||
|
radius: number;
|
||||||
|
color: { r: number; g: number; b: number; a: number };
|
||||||
|
layer: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 粒子效果数据 */
|
||||||
|
export interface ParticleEffect {
|
||||||
|
position: Vector2;
|
||||||
|
velocity: Vector2;
|
||||||
|
color: { r: number; g: number; b: number; a: number };
|
||||||
|
lifetime: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRenderer {
|
||||||
|
/**
|
||||||
|
* 初始化渲染器
|
||||||
|
*/
|
||||||
|
initialize(parentNode: any): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染物理体
|
||||||
|
*/
|
||||||
|
renderBodies(bodies: PhysicsBodyData[]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建渲染对象
|
||||||
|
*/
|
||||||
|
createRenderObject(body: PhysicsBodyData): RenderObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新渲染对象
|
||||||
|
*/
|
||||||
|
updateRenderObject(renderObject: RenderObject, body: PhysicsBodyData): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除渲染对象
|
||||||
|
*/
|
||||||
|
removeRenderObject(bodyId: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放粒子效果
|
||||||
|
*/
|
||||||
|
playParticleEffect(effect: ParticleEffect): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有渲染对象
|
||||||
|
*/
|
||||||
|
clear(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置相机
|
||||||
|
*/
|
||||||
|
setCamera(camera: any): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置世界边界
|
||||||
|
*/
|
||||||
|
setWorldBounds(width: number, height: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置相机位置
|
||||||
|
*/
|
||||||
|
setCameraPosition(position: Vector2): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置相机缩放
|
||||||
|
*/
|
||||||
|
setCameraZoom(zoom: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁渲染器
|
||||||
|
*/
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "afa401be-4482-4d0a-ae6c-1b1b94b7b992",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "c0d94965-94bc-44ed-b580-d2b93cdb93a1",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
import { _decorator, Camera, Component, Node } from 'cc';
|
||||||
|
import { EventBus } from '../Core/EventBus';
|
||||||
|
import { PhysicsBodyData, Vector2 } from '../Core/GameData';
|
||||||
|
import { IPhysicsEngine } from '../Core/IPhysicsEngine';
|
||||||
|
import { IRenderer } from '../Core/IRenderer';
|
||||||
|
import { InputManager } from '../Input/InputManager';
|
||||||
|
import { MouseInputEvent, TouchInputEvent } from '../Input/InputTypes';
|
||||||
|
import { WasmPhysicsEngine } from '../Physics/WasmPhysicsEngine';
|
||||||
|
import { PinballRenderer } from '../Renderer/PinballRenderer';
|
||||||
|
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone模式 - 单机弹珠物理游戏模式
|
||||||
|
* 整合物理引擎、渲染器和输入管理器,提供完整的单机游戏体验
|
||||||
|
*/
|
||||||
|
@ccclass('StandaloneMode')
|
||||||
|
export class StandaloneMode extends Component {
|
||||||
|
|
||||||
|
@property(Camera)
|
||||||
|
gameCamera: Camera = null;
|
||||||
|
|
||||||
|
@property(Node)
|
||||||
|
renderNode: Node = null;
|
||||||
|
|
||||||
|
@property({ type: Node, tooltip: "用于显示游戏边界的节点" })
|
||||||
|
boundsNode: Node = null;
|
||||||
|
|
||||||
|
// 核心系统
|
||||||
|
private physicsEngine: IPhysicsEngine = null;
|
||||||
|
private renderer: IRenderer = null;
|
||||||
|
private inputManager: InputManager = null;
|
||||||
|
private eventBus: EventBus = null;
|
||||||
|
|
||||||
|
// 游戏配置
|
||||||
|
@property({ tooltip: "游戏世界宽度" })
|
||||||
|
worldWidth: number = 800;
|
||||||
|
|
||||||
|
@property({ tooltip: "游戏世界高度" })
|
||||||
|
worldHeight: number = 600;
|
||||||
|
|
||||||
|
@property({ tooltip: "重力加速度" })
|
||||||
|
gravity: number = -9.81;
|
||||||
|
|
||||||
|
@property({ tooltip: "弹珠默认半径" })
|
||||||
|
ballRadius: number = 10;
|
||||||
|
|
||||||
|
@property({ tooltip: "弹珠默认密度" })
|
||||||
|
ballDensity: number = 1.0;
|
||||||
|
|
||||||
|
@property({ tooltip: "弹珠默认弹性系数" })
|
||||||
|
ballRestitution: number = 0.8;
|
||||||
|
|
||||||
|
// 游戏状态
|
||||||
|
private isInitialized: boolean = false;
|
||||||
|
private ballCount: number = 0;
|
||||||
|
private activeBalls: Map<number, PhysicsBodyData> = new Map();
|
||||||
|
|
||||||
|
async onLoad() {
|
||||||
|
// 初始化事件总线
|
||||||
|
this.eventBus = EventBus.getInstance();
|
||||||
|
|
||||||
|
// 初始化物理引擎
|
||||||
|
await this.initializePhysics();
|
||||||
|
|
||||||
|
// 初始化渲染器
|
||||||
|
this.initializeRenderer();
|
||||||
|
|
||||||
|
// 初始化输入管理器
|
||||||
|
this.initializeInput();
|
||||||
|
|
||||||
|
// 注册事件监听
|
||||||
|
this.registerEventHandlers();
|
||||||
|
|
||||||
|
console.log('[StandaloneMode] 初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnable() {
|
||||||
|
if (this.isInitialized) {
|
||||||
|
this.startGameLoop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDisable() {
|
||||||
|
this.stopGameLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
this.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化物理引擎
|
||||||
|
*/
|
||||||
|
private async initializePhysics(): Promise<void> {
|
||||||
|
this.physicsEngine = new WasmPhysicsEngine();
|
||||||
|
await this.physicsEngine.initialize({
|
||||||
|
gravity: { x: 0, y: this.gravity },
|
||||||
|
timeStep: 1 / 60,
|
||||||
|
maxBodies: 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建物理世界
|
||||||
|
await this.physicsEngine.createWorld({ x: 0, y: this.gravity });
|
||||||
|
|
||||||
|
// 创建世界边界
|
||||||
|
this.createWorldBounds();
|
||||||
|
|
||||||
|
console.log('[StandaloneMode] 物理引擎初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化渲染器
|
||||||
|
*/
|
||||||
|
private initializeRenderer(): void {
|
||||||
|
if (!this.renderNode) {
|
||||||
|
console.error('[StandaloneMode] renderNode 未设置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderer = this.renderNode.getComponent(PinballRenderer);
|
||||||
|
if (!this.renderer) {
|
||||||
|
this.renderer = this.renderNode.addComponent(PinballRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置渲染器参数
|
||||||
|
if (this.gameCamera) {
|
||||||
|
this.renderer.setCamera(this.gameCamera);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderer.setWorldBounds(this.worldWidth, this.worldHeight);
|
||||||
|
|
||||||
|
console.log('[StandaloneMode] 渲染器初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化输入管理器
|
||||||
|
*/
|
||||||
|
private initializeInput(): void {
|
||||||
|
this.inputManager = this.getComponent(InputManager);
|
||||||
|
if (!this.inputManager) {
|
||||||
|
this.inputManager = this.addComponent(InputManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置输入管理器参数
|
||||||
|
if (this.gameCamera) {
|
||||||
|
this.inputManager.setCamera(this.gameCamera);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[StandaloneMode] 输入管理器初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册事件处理器
|
||||||
|
*/
|
||||||
|
private registerEventHandlers(): void {
|
||||||
|
// 监听输入事件
|
||||||
|
this.eventBus.on('input.mouse.click', (event: MouseInputEvent) => this.onMouseClick(event));
|
||||||
|
this.eventBus.on('input.touch.start', (event: TouchInputEvent) => this.onTouchStart(event));
|
||||||
|
|
||||||
|
// 监听物理事件
|
||||||
|
this.eventBus.on('physics.collision', (collisionData: any) => this.onPhysicsCollision(collisionData));
|
||||||
|
|
||||||
|
console.log('[StandaloneMode] 事件处理器注册完成');
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建世界边界
|
||||||
|
*/
|
||||||
|
private createWorldBounds(): void {
|
||||||
|
const halfWidth = this.worldWidth / 2;
|
||||||
|
const halfHeight = this.worldHeight / 2;
|
||||||
|
const wallThickness = 10;
|
||||||
|
|
||||||
|
// 创建四面墙壁
|
||||||
|
const walls = [
|
||||||
|
// 底部墙
|
||||||
|
{ x: 0, y: -halfHeight - wallThickness / 2, width: this.worldWidth + wallThickness, height: wallThickness },
|
||||||
|
// 顶部墙
|
||||||
|
{ x: 0, y: halfHeight + wallThickness / 2, width: this.worldWidth + wallThickness, height: wallThickness },
|
||||||
|
// 左侧墙
|
||||||
|
{ x: -halfWidth - wallThickness / 2, y: 0, width: wallThickness, height: this.worldHeight + wallThickness },
|
||||||
|
// 右侧墙
|
||||||
|
{ x: halfWidth + wallThickness / 2, y: 0, width: wallThickness, height: this.worldHeight + wallThickness }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const wall of walls) {
|
||||||
|
this.physicsEngine.createBox({
|
||||||
|
position: { x: wall.x, y: wall.y },
|
||||||
|
size: { x: wall.width, y: wall.height },
|
||||||
|
isStatic: true,
|
||||||
|
restitution: 0.8,
|
||||||
|
friction: 0.3
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[StandaloneMode] 世界边界创建完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标点击事件处理
|
||||||
|
*/
|
||||||
|
private onMouseClick(event: MouseInputEvent): void {
|
||||||
|
this.createBallAtPosition(event.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触摸开始事件处理
|
||||||
|
*/
|
||||||
|
private onTouchStart(event: TouchInputEvent): void {
|
||||||
|
this.createBallAtPosition(event.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在指定位置创建弹珠
|
||||||
|
*/
|
||||||
|
private createBallAtPosition(position: Vector2): void {
|
||||||
|
const ballId = this.physicsEngine.createCircle({
|
||||||
|
position: position,
|
||||||
|
radius: this.ballRadius,
|
||||||
|
isStatic: false,
|
||||||
|
density: this.ballDensity,
|
||||||
|
restitution: this.ballRestitution,
|
||||||
|
friction: 0.3
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建球体数据
|
||||||
|
const ballData: PhysicsBodyData = {
|
||||||
|
id: ballId,
|
||||||
|
position: position,
|
||||||
|
rotation: 0,
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
angularVelocity: 0,
|
||||||
|
bodyType: 'circle',
|
||||||
|
radius: this.ballRadius,
|
||||||
|
isStatic: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.activeBalls.set(ballId, ballData);
|
||||||
|
this.ballCount++;
|
||||||
|
|
||||||
|
// 通知渲染器
|
||||||
|
this.eventBus.emit('ball.created', {
|
||||||
|
id: ballId,
|
||||||
|
position: position,
|
||||||
|
radius: this.ballRadius
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[StandaloneMode] 创建弹珠 #${ballId} 在位置 (${position.x}, ${position.y})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物理碰撞事件处理
|
||||||
|
*/
|
||||||
|
private onPhysicsCollision(collisionData: any): void {
|
||||||
|
// 播放碰撞音效或效果
|
||||||
|
console.log('[StandaloneMode] 物理碰撞:', collisionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始游戏循环
|
||||||
|
*/
|
||||||
|
private startGameLoop(): void {
|
||||||
|
// 在update中已经自动运行物理和渲染循环
|
||||||
|
console.log('[StandaloneMode] 游戏循环已开始');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止游戏循环
|
||||||
|
*/
|
||||||
|
private stopGameLoop(): void {
|
||||||
|
console.log('[StandaloneMode] 游戏循环已停止');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏更新循环
|
||||||
|
*/
|
||||||
|
update(deltaTime: number) {
|
||||||
|
if (!this.isInitialized || !this.physicsEngine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步进物理引擎
|
||||||
|
this.physicsEngine.step(deltaTime);
|
||||||
|
|
||||||
|
// 更新所有活动球体的状态
|
||||||
|
for (const [ballId, ballData] of this.activeBalls) {
|
||||||
|
const updatedData = this.physicsEngine.getBodyData(ballId);
|
||||||
|
if (updatedData) {
|
||||||
|
// 更新本地数据
|
||||||
|
ballData.position = updatedData.position;
|
||||||
|
ballData.rotation = updatedData.rotation;
|
||||||
|
ballData.velocity = updatedData.velocity;
|
||||||
|
ballData.angularVelocity = updatedData.angularVelocity;
|
||||||
|
|
||||||
|
// 发送更新事件给渲染器
|
||||||
|
this.eventBus.emit('ball.updated', {
|
||||||
|
id: ballId,
|
||||||
|
position: ballData.position,
|
||||||
|
rotation: ballData.rotation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源
|
||||||
|
*/
|
||||||
|
private cleanup(): void {
|
||||||
|
// 清理事件监听器
|
||||||
|
this.eventBus.off('input.mouse.click', this.onMouseClick);
|
||||||
|
this.eventBus.off('input.touch.start', this.onTouchStart);
|
||||||
|
this.eventBus.off('physics.collision', this.onPhysicsCollision);
|
||||||
|
|
||||||
|
// 清理物理引擎
|
||||||
|
if (this.physicsEngine) {
|
||||||
|
this.physicsEngine.cleanup();
|
||||||
|
this.physicsEngine = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理数据
|
||||||
|
this.activeBalls.clear();
|
||||||
|
this.ballCount = 0;
|
||||||
|
this.isInitialized = false;
|
||||||
|
|
||||||
|
console.log('[StandaloneMode] 资源清理完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取游戏统计信息
|
||||||
|
*/
|
||||||
|
public getGameStats() {
|
||||||
|
return {
|
||||||
|
ballCount: this.ballCount,
|
||||||
|
activeBalls: this.activeBalls.size,
|
||||||
|
worldSize: { width: this.worldWidth, height: this.worldHeight },
|
||||||
|
isInitialized: this.isInitialized
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置游戏
|
||||||
|
*/
|
||||||
|
public resetGame(): void {
|
||||||
|
// 移除所有球体
|
||||||
|
for (const ballId of this.activeBalls.keys()) {
|
||||||
|
this.physicsEngine.removeBody(ballId);
|
||||||
|
this.eventBus.emit('ball.removed', { id: ballId });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeBalls.clear();
|
||||||
|
this.ballCount = 0;
|
||||||
|
|
||||||
|
console.log('[StandaloneMode] 游戏重置完成');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "f1a5b7ea-5782-43d3-9eea-8b5909a93dbd",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
9
client-cocos/assets/scripts/Modules/Pinball/Input.meta
Normal file
9
client-cocos/assets/scripts/Modules/Pinball/Input.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "2121ed3d-9094-4dde-97cb-7ba9081cbd9d",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
/**
|
||||||
|
* 输入管理器实现
|
||||||
|
* 处理鼠标和触摸输入,转换为游戏事件
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Camera, Component, EventMouse, EventTouch, Vec2, Vec3, _decorator } from 'cc';
|
||||||
|
import { EventBus } from '../Core/EventBus';
|
||||||
|
import { Vector2 } from '../Core/GameData';
|
||||||
|
import {
|
||||||
|
BaseInputEvent,
|
||||||
|
InputButton,
|
||||||
|
InputCallback,
|
||||||
|
InputConfig,
|
||||||
|
InputEventType,
|
||||||
|
InputState,
|
||||||
|
MouseInputEvent,
|
||||||
|
TouchInputEvent
|
||||||
|
} from './InputTypes';
|
||||||
|
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
@ccclass
|
||||||
|
export class InputManager extends Component {
|
||||||
|
|
||||||
|
@property({ tooltip: '启用鼠标输入' })
|
||||||
|
enableMouse: boolean = true;
|
||||||
|
|
||||||
|
@property({ tooltip: '启用触摸输入' })
|
||||||
|
enableTouch: boolean = true;
|
||||||
|
|
||||||
|
@property({ tooltip: '双击时间间隔(ms)' })
|
||||||
|
doubleClickTime: number = 300;
|
||||||
|
|
||||||
|
@property({ tooltip: '长按时间(ms)' })
|
||||||
|
longPressTime: number = 500;
|
||||||
|
|
||||||
|
@property({ tooltip: '拖拽阈值(像素)' })
|
||||||
|
dragThreshold: number = 10;
|
||||||
|
|
||||||
|
private eventBus: EventBus;
|
||||||
|
private camera: Camera | null = null;
|
||||||
|
private inputState: InputState;
|
||||||
|
private config: InputConfig;
|
||||||
|
private callbacks: Map<InputEventType, InputCallback[]> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件初始化
|
||||||
|
*/
|
||||||
|
onLoad(): void {
|
||||||
|
this.eventBus = EventBus.getInstance();
|
||||||
|
this.initializeInputState();
|
||||||
|
this.initializeConfig();
|
||||||
|
this.setupInputEvents();
|
||||||
|
|
||||||
|
console.log('InputManager initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化输入状态
|
||||||
|
*/
|
||||||
|
private initializeInputState(): void {
|
||||||
|
this.inputState = {
|
||||||
|
isMouseDown: false,
|
||||||
|
isTouchActive: false,
|
||||||
|
lastClickTime: 0,
|
||||||
|
lastClickPosition: { x: 0, y: 0 },
|
||||||
|
isDragging: false,
|
||||||
|
dragStartPosition: { x: 0, y: 0 },
|
||||||
|
activeTouches: new Map()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化配置
|
||||||
|
*/
|
||||||
|
private initializeConfig(): void {
|
||||||
|
this.config = {
|
||||||
|
enableMouse: this.enableMouse,
|
||||||
|
enableTouch: this.enableTouch,
|
||||||
|
doubleClickTime: this.doubleClickTime,
|
||||||
|
longPressTime: this.longPressTime,
|
||||||
|
dragThreshold: this.dragThreshold
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置相机引用
|
||||||
|
*/
|
||||||
|
setCamera(camera: Camera): void {
|
||||||
|
this.camera = camera;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件回调
|
||||||
|
*/
|
||||||
|
on<T extends BaseInputEvent>(eventType: InputEventType, callback: InputCallback<T>): void {
|
||||||
|
if (!this.callbacks.has(eventType)) {
|
||||||
|
this.callbacks.set(eventType, []);
|
||||||
|
}
|
||||||
|
this.callbacks.get(eventType)!.push(callback as InputCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除事件回调
|
||||||
|
*/
|
||||||
|
off<T extends BaseInputEvent>(eventType: InputEventType, callback: InputCallback<T>): void {
|
||||||
|
const callbacks = this.callbacks.get(eventType);
|
||||||
|
if (callbacks) {
|
||||||
|
const index = callbacks.indexOf(callback as InputCallback);
|
||||||
|
if (index > -1) {
|
||||||
|
callbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置输入事件监听
|
||||||
|
*/
|
||||||
|
private setupInputEvents(): void {
|
||||||
|
if (this.config.enableMouse) {
|
||||||
|
this.node.on('mousedown', this.onMouseDown, this);
|
||||||
|
this.node.on('mouseup', this.onMouseUp, this);
|
||||||
|
this.node.on('mousemove', this.onMouseMove, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.enableTouch) {
|
||||||
|
this.node.on('touchstart', this.onTouchStart, this);
|
||||||
|
this.node.on('touchend', this.onTouchEnd, this);
|
||||||
|
this.node.on('touchmove', this.onTouchMove, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标按下事件
|
||||||
|
*/
|
||||||
|
private onMouseDown(event: EventMouse): void {
|
||||||
|
const position = this.screenToWorldPosition(event.getUILocation());
|
||||||
|
const inputEvent: MouseInputEvent = {
|
||||||
|
type: InputEventType.MOUSE_DOWN,
|
||||||
|
position,
|
||||||
|
screenPosition: { x: event.getUILocation().x, y: event.getUILocation().y },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
button: event.getButton() as InputButton,
|
||||||
|
ctrlKey: false, // Cocos Creator 3.x 中需要通过其他方式获取
|
||||||
|
shiftKey: false,
|
||||||
|
altKey: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.inputState.isMouseDown = true;
|
||||||
|
this.inputState.dragStartPosition = position;
|
||||||
|
|
||||||
|
// 检测双击
|
||||||
|
const timeSinceLastClick = inputEvent.timestamp - this.inputState.lastClickTime;
|
||||||
|
if (timeSinceLastClick < this.config.doubleClickTime) {
|
||||||
|
const distance = this.calculateDistance(position, this.inputState.lastClickPosition);
|
||||||
|
if (distance < this.config.dragThreshold) {
|
||||||
|
this.emitDoubleClickEvent(inputEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inputState.lastClickTime = inputEvent.timestamp;
|
||||||
|
this.inputState.lastClickPosition = position;
|
||||||
|
|
||||||
|
this.emitEvent(inputEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标释放事件
|
||||||
|
*/
|
||||||
|
private onMouseUp(event: EventMouse): void {
|
||||||
|
const position = this.screenToWorldPosition(event.getUILocation());
|
||||||
|
const inputEvent: MouseInputEvent = {
|
||||||
|
type: InputEventType.MOUSE_UP,
|
||||||
|
position,
|
||||||
|
screenPosition: { x: event.getUILocation().x, y: event.getUILocation().y },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
button: event.getButton() as InputButton,
|
||||||
|
ctrlKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
altKey: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.inputState.isMouseDown = false;
|
||||||
|
this.inputState.isDragging = false;
|
||||||
|
|
||||||
|
this.emitEvent(inputEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标移动事件
|
||||||
|
*/
|
||||||
|
private onMouseMove(event: EventMouse): void {
|
||||||
|
const position = this.screenToWorldPosition(event.getUILocation());
|
||||||
|
const inputEvent: MouseInputEvent = {
|
||||||
|
type: InputEventType.MOUSE_MOVE,
|
||||||
|
position,
|
||||||
|
screenPosition: { x: event.getUILocation().x, y: event.getUILocation().y },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
button: event.getButton() as InputButton,
|
||||||
|
ctrlKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
altKey: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检测拖拽开始
|
||||||
|
if (this.inputState.isMouseDown && !this.inputState.isDragging) {
|
||||||
|
const distance = this.calculateDistance(position, this.inputState.dragStartPosition);
|
||||||
|
if (distance > this.config.dragThreshold) {
|
||||||
|
this.inputState.isDragging = true;
|
||||||
|
this.emitDragStartEvent(inputEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emitEvent(inputEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触摸开始事件
|
||||||
|
*/
|
||||||
|
private onTouchStart(event: EventTouch): void {
|
||||||
|
const touches = event.getAllTouches();
|
||||||
|
|
||||||
|
for (const touch of touches) {
|
||||||
|
const position = this.screenToWorldPosition(touch.getUILocation());
|
||||||
|
const touchEvent: TouchInputEvent = {
|
||||||
|
type: InputEventType.TOUCH_START,
|
||||||
|
position,
|
||||||
|
screenPosition: { x: touch.getUILocation().x, y: touch.getUILocation().y },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
touchId: touch.getID(),
|
||||||
|
force: undefined // Cocos Creator 3.x 中 getForce 方法可能不可用
|
||||||
|
};
|
||||||
|
|
||||||
|
this.inputState.activeTouches.set(touch.getID(), touchEvent);
|
||||||
|
this.inputState.isTouchActive = true;
|
||||||
|
|
||||||
|
this.emitEvent(touchEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触摸结束事件
|
||||||
|
*/
|
||||||
|
private onTouchEnd(event: EventTouch): void {
|
||||||
|
const touches = event.getAllTouches();
|
||||||
|
|
||||||
|
for (const touch of touches) {
|
||||||
|
const position = this.screenToWorldPosition(touch.getUILocation());
|
||||||
|
const touchEvent: TouchInputEvent = {
|
||||||
|
type: InputEventType.TOUCH_END,
|
||||||
|
position,
|
||||||
|
screenPosition: { x: touch.getUILocation().x, y: touch.getUILocation().y },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
touchId: touch.getID(),
|
||||||
|
force: undefined // Cocos Creator 3.x 中 getForce 方法可能不可用
|
||||||
|
};
|
||||||
|
|
||||||
|
this.inputState.activeTouches.delete(touch.getID());
|
||||||
|
if (this.inputState.activeTouches.size === 0) {
|
||||||
|
this.inputState.isTouchActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emitEvent(touchEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触摸移动事件
|
||||||
|
*/
|
||||||
|
private onTouchMove(event: EventTouch): void {
|
||||||
|
const touches = event.getAllTouches();
|
||||||
|
|
||||||
|
for (const touch of touches) {
|
||||||
|
const position = this.screenToWorldPosition(touch.getUILocation());
|
||||||
|
const touchEvent: TouchInputEvent = {
|
||||||
|
type: InputEventType.TOUCH_MOVE,
|
||||||
|
position,
|
||||||
|
screenPosition: { x: touch.getUILocation().x, y: touch.getUILocation().y },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
touchId: touch.getID(),
|
||||||
|
force: undefined // Cocos Creator 3.x 中 getForce 方法可能不可用
|
||||||
|
};
|
||||||
|
|
||||||
|
this.emitEvent(touchEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 屏幕坐标转世界坐标
|
||||||
|
*/
|
||||||
|
private screenToWorldPosition(screenPos: Vec2): Vector2 {
|
||||||
|
if (!this.camera) {
|
||||||
|
// 如果没有相机,使用简单的坐标转换
|
||||||
|
return {
|
||||||
|
x: (screenPos.x - 400) / 100, // 假设屏幕中心为 (400, 300),缩放 100倍
|
||||||
|
y: (300 - screenPos.y) / 100
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用相机进行坐标转换 - 转换为 Vec3
|
||||||
|
const screenPos3 = new Vec3(screenPos.x, screenPos.y, 0);
|
||||||
|
const worldPos = this.camera.screenToWorld(screenPos3);
|
||||||
|
return {
|
||||||
|
x: worldPos.x / 100, // 转换为物理世界坐标
|
||||||
|
y: worldPos.y / 100
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算两点距离
|
||||||
|
*/
|
||||||
|
private calculateDistance(pos1: Vector2, pos2: Vector2): number {
|
||||||
|
const dx = pos1.x - pos2.x;
|
||||||
|
const dy = pos1.y - pos2.y;
|
||||||
|
return Math.sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发射事件
|
||||||
|
*/
|
||||||
|
private emitEvent(event: BaseInputEvent): void {
|
||||||
|
const callbacks = this.callbacks.get(event.type);
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in input callback for ${event.type}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同时通过事件总线发射
|
||||||
|
this.eventBus.emit(event.type, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发射双击事件
|
||||||
|
*/
|
||||||
|
private emitDoubleClickEvent(event: MouseInputEvent): void {
|
||||||
|
this.eventBus.emit('double_click', event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发射拖拽开始事件
|
||||||
|
*/
|
||||||
|
private emitDragStartEvent(event: MouseInputEvent): void {
|
||||||
|
this.eventBus.emit('drag_start', event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前输入状态
|
||||||
|
*/
|
||||||
|
getInputState(): InputState {
|
||||||
|
return { ...this.inputState };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件总线
|
||||||
|
*/
|
||||||
|
getEventBus(): EventBus {
|
||||||
|
return this.eventBus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源
|
||||||
|
*/
|
||||||
|
onDestroy(): void {
|
||||||
|
// 移除事件监听
|
||||||
|
this.node.off('mousedown', this.onMouseDown, this);
|
||||||
|
this.node.off('mouseup', this.onMouseUp, this);
|
||||||
|
this.node.off('mousemove', this.onMouseMove, this);
|
||||||
|
this.node.off('touchstart', this.onTouchStart, this);
|
||||||
|
this.node.off('touchend', this.onTouchEnd, this);
|
||||||
|
this.node.off('touchmove', this.onTouchMove, this);
|
||||||
|
|
||||||
|
// 清理回调
|
||||||
|
this.callbacks.clear();
|
||||||
|
|
||||||
|
// 清理事件总线
|
||||||
|
this.eventBus.clear();
|
||||||
|
|
||||||
|
console.log('InputManager destroyed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "1a3cf9f0-ddcf-4175-9dc8-632e89c6941e",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* 输入相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Vector2 } from '../Core/GameData';
|
||||||
|
|
||||||
|
/** 输入事件类型 */
|
||||||
|
export enum InputEventType {
|
||||||
|
MOUSE_DOWN = 'mouse_down',
|
||||||
|
MOUSE_UP = 'mouse_up',
|
||||||
|
MOUSE_MOVE = 'mouse_move',
|
||||||
|
TOUCH_START = 'touch_start',
|
||||||
|
TOUCH_END = 'touch_end',
|
||||||
|
TOUCH_MOVE = 'touch_move'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 输入按键枚举 */
|
||||||
|
export enum InputButton {
|
||||||
|
LEFT_MOUSE = 0,
|
||||||
|
RIGHT_MOUSE = 1,
|
||||||
|
MIDDLE_MOUSE = 2,
|
||||||
|
TOUCH = 99
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 基础输入事件数据 */
|
||||||
|
export interface BaseInputEvent {
|
||||||
|
type: InputEventType;
|
||||||
|
position: Vector2; // 世界坐标
|
||||||
|
screenPosition: Vector2; // 屏幕坐标
|
||||||
|
timestamp: number;
|
||||||
|
button?: InputButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 鼠标输入事件 */
|
||||||
|
export interface MouseInputEvent extends BaseInputEvent {
|
||||||
|
type: InputEventType.MOUSE_DOWN | InputEventType.MOUSE_UP | InputEventType.MOUSE_MOVE;
|
||||||
|
button: InputButton;
|
||||||
|
ctrlKey: boolean;
|
||||||
|
shiftKey: boolean;
|
||||||
|
altKey: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 触摸输入事件 */
|
||||||
|
export interface TouchInputEvent extends BaseInputEvent {
|
||||||
|
type: InputEventType.TOUCH_START | InputEventType.TOUCH_END | InputEventType.TOUCH_MOVE;
|
||||||
|
touchId: number;
|
||||||
|
force?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 输入配置 */
|
||||||
|
export interface InputConfig {
|
||||||
|
enableMouse: boolean;
|
||||||
|
enableTouch: boolean;
|
||||||
|
doubleClickTime: number; // 双击时间间隔 (ms)
|
||||||
|
longPressTime: number; // 长按时间 (ms)
|
||||||
|
dragThreshold: number; // 拖拽阈值 (像素)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 输入回调函数类型 */
|
||||||
|
export type InputCallback<T extends BaseInputEvent = BaseInputEvent> = (event: T) => void;
|
||||||
|
|
||||||
|
/** 输入状态 */
|
||||||
|
export interface InputState {
|
||||||
|
isMouseDown: boolean;
|
||||||
|
isTouchActive: boolean;
|
||||||
|
lastClickTime: number;
|
||||||
|
lastClickPosition: Vector2;
|
||||||
|
isDragging: boolean;
|
||||||
|
dragStartPosition: Vector2;
|
||||||
|
activeTouches: Map<number, TouchInputEvent>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "b07a5b72-454a-4fb1-8dd6-9b39141b85d8",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
9
client-cocos/assets/scripts/Modules/Pinball/Network.meta
Normal file
9
client-cocos/assets/scripts/Modules/Pinball/Network.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "3cc0736e-7d1c-4771-b861-bfd3525e90d1",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
9
client-cocos/assets/scripts/Modules/Pinball/Physics.meta
Normal file
9
client-cocos/assets/scripts/Modules/Pinball/Physics.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "0d17157f-5edf-479c-9296-4008cd31be83",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* 物理引擎相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Vector2 } from '../Core/GameData';
|
||||||
|
|
||||||
|
/** WASM 函数签名定义 */
|
||||||
|
export interface WasmExports {
|
||||||
|
// 世界管理
|
||||||
|
pinball_create_world(gravity_x: number, gravity_y: number): number;
|
||||||
|
pinball_step_world(world_id: number): void;
|
||||||
|
|
||||||
|
// 刚体管理
|
||||||
|
pinball_create_dynamic_body(world_id: number, x: number, y: number): number;
|
||||||
|
|
||||||
|
// 位置获取
|
||||||
|
pinball_get_body_x(world_id: number, body_id: number): number;
|
||||||
|
pinball_get_body_y(world_id: number, body_id: number): number;
|
||||||
|
|
||||||
|
// 内存管理
|
||||||
|
memory: WebAssembly.Memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WASM 模块状态 */
|
||||||
|
export enum WasmModuleState {
|
||||||
|
UNLOADED = 'unloaded',
|
||||||
|
LOADING = 'loading',
|
||||||
|
LOADED = 'loaded',
|
||||||
|
ERROR = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WASM 配置 */
|
||||||
|
export interface WasmConfig {
|
||||||
|
wasmPath: string;
|
||||||
|
memoryPages?: number;
|
||||||
|
importObject?: WebAssembly.Imports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 物理世界配置 */
|
||||||
|
export interface PhysicsWorldConfig {
|
||||||
|
gravity: Vector2;
|
||||||
|
timeStep: number;
|
||||||
|
velocityIterations: number;
|
||||||
|
positionIterations: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 碰撞体类型 */
|
||||||
|
export enum ColliderType {
|
||||||
|
CIRCLE = 'circle',
|
||||||
|
BOX = 'box'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 碰撞体定义 */
|
||||||
|
export interface ColliderDef {
|
||||||
|
type: ColliderType;
|
||||||
|
radius?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
offset?: Vector2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 刚体类型 */
|
||||||
|
export enum BodyType {
|
||||||
|
STATIC = 'static',
|
||||||
|
DYNAMIC = 'dynamic',
|
||||||
|
KINEMATIC = 'kinematic'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 刚体定义 */
|
||||||
|
export interface BodyDef {
|
||||||
|
type: BodyType;
|
||||||
|
position: Vector2;
|
||||||
|
velocity?: Vector2;
|
||||||
|
colliders: ColliderDef[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "6a80be32-a5d9-4b97-ace6-ac2c417a91b3",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
/**
|
||||||
|
* WASM 物理引擎实现
|
||||||
|
* 提供与 pinball-physics WASM 模块的接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { wasmLoader } from '../../../Utils/WasmLoader';
|
||||||
|
import { BodyId, PhysicsBodyData, PhysicsSettings, Vector2, WorldId } from '../Core/GameData';
|
||||||
|
import { IPhysicsEngine } from '../Core/IPhysicsEngine';
|
||||||
|
import { WasmExports, WasmModuleState } from './PhysicsTypes';
|
||||||
|
|
||||||
|
export class WasmPhysicsEngine implements IPhysicsEngine {
|
||||||
|
private wasmModule: WebAssembly.Module | null = null;
|
||||||
|
private wasmInstance: WebAssembly.Instance | null = null;
|
||||||
|
private wasmExports: WasmExports | null = null;
|
||||||
|
private state: WasmModuleState = WasmModuleState.UNLOADED;
|
||||||
|
private worlds: Map<WorldId, boolean> = new Map();
|
||||||
|
private bodies: Map<WorldId, Map<BodyId, boolean>> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 WASM 物理引擎
|
||||||
|
* @param settings 物理设置
|
||||||
|
* @param wasmFactory WASM工厂函数(可选,推荐使用)
|
||||||
|
*/
|
||||||
|
async initialize(settings: PhysicsSettings, wasmFactory?: any): Promise<void> {
|
||||||
|
if (this.state === WasmModuleState.LOADED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = WasmModuleState.LOADING;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (wasmFactory) {
|
||||||
|
// 使用新的 WasmLoader(推荐方式)
|
||||||
|
await this.initializeWithWasmLoader(wasmFactory);
|
||||||
|
} else {
|
||||||
|
// 回退到旧的加载方式(保持向后兼容)
|
||||||
|
console.warn('使用旧的 WASM 加载方式,推荐提供 wasmFactory 参数使用 WasmLoader');
|
||||||
|
await this.initializeLegacy();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = WasmModuleState.LOADED;
|
||||||
|
console.log('WASM Physics Engine initialized successfully');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.state = WasmModuleState.ERROR;
|
||||||
|
console.error('Failed to initialize WASM Physics Engine:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 WASM 导入对象
|
||||||
|
*/
|
||||||
|
private createImportObject(): WebAssembly.Imports {
|
||||||
|
return {
|
||||||
|
env: {
|
||||||
|
// 内存管理
|
||||||
|
memory: new WebAssembly.Memory({
|
||||||
|
initial: 10,
|
||||||
|
maximum: 100
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 日志函数
|
||||||
|
console_log: (ptr: number, len: number) => {
|
||||||
|
// 可以实现 WASM 的日志输出
|
||||||
|
},
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
abort: (msg: number, file: number, line: number, col: number) => {
|
||||||
|
console.error('WASM abort:', { msg, file, line, col });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 WasmLoader 初始化(推荐方式)
|
||||||
|
*/
|
||||||
|
private async initializeWithWasmLoader(wasmFactory: any): Promise<void> {
|
||||||
|
// 初始化 WASM 加载器
|
||||||
|
wasmLoader.initialize();
|
||||||
|
|
||||||
|
// 检查平台支持
|
||||||
|
if (!wasmLoader.isWasmSupported()) {
|
||||||
|
throw new Error('当前平台不支持 WASM,需要提供 ASM 回退选项');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载 WASM 模块
|
||||||
|
// 注意:在实际使用时需要提供正确的 editorUuid
|
||||||
|
const instance = await wasmLoader.loadSimpleWasm(
|
||||||
|
wasmFactory,
|
||||||
|
'pinball_physics.wasm',
|
||||||
|
undefined, // editorUuid,需要在实际使用时提供
|
||||||
|
'wasm' // bundleName
|
||||||
|
);
|
||||||
|
|
||||||
|
this.wasmExports = instance as WasmExports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 旧的初始化方法(保持向后兼容)
|
||||||
|
*/
|
||||||
|
private async initializeLegacy(): Promise<void> {
|
||||||
|
// 加载 WASM 文件
|
||||||
|
const wasmPath = 'assets/wasm/pinball_physics.wasm';
|
||||||
|
const wasmResponse = await fetch(wasmPath);
|
||||||
|
|
||||||
|
if (!wasmResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch WASM file: ${wasmResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasmBytes = await wasmResponse.arrayBuffer();
|
||||||
|
|
||||||
|
// 编译 WASM 模块
|
||||||
|
this.wasmModule = await WebAssembly.compile(wasmBytes);
|
||||||
|
|
||||||
|
// 创建实例
|
||||||
|
const importObject = this.createImportObject();
|
||||||
|
this.wasmInstance = await WebAssembly.instantiate(this.wasmModule, importObject);
|
||||||
|
this.wasmExports = this.wasmInstance.exports as unknown as WasmExports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 WASM 是否已加载
|
||||||
|
*/
|
||||||
|
private ensureLoaded(): void {
|
||||||
|
if (this.state !== WasmModuleState.LOADED || !this.wasmExports) {
|
||||||
|
throw new Error('WASM Physics Engine not loaded. Call initialize() first.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建物理世界
|
||||||
|
*/
|
||||||
|
async createWorld(gravity: Vector2): Promise<WorldId> {
|
||||||
|
this.ensureLoaded();
|
||||||
|
|
||||||
|
const worldId = this.wasmExports!.pinball_create_world(gravity.x, gravity.y);
|
||||||
|
this.worlds.set(worldId, true);
|
||||||
|
this.bodies.set(worldId, new Map());
|
||||||
|
|
||||||
|
console.log(`Created physics world ${worldId} with gravity (${gravity.x}, ${gravity.y})`);
|
||||||
|
return worldId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁物理世界
|
||||||
|
*/
|
||||||
|
async destroyWorld(worldId: WorldId): Promise<void> {
|
||||||
|
if (this.worlds.has(worldId)) {
|
||||||
|
this.worlds.delete(worldId);
|
||||||
|
this.bodies.delete(worldId);
|
||||||
|
console.log(`Destroyed physics world ${worldId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行物理步进
|
||||||
|
*/
|
||||||
|
async step(worldId: WorldId): Promise<void> {
|
||||||
|
this.ensureLoaded();
|
||||||
|
|
||||||
|
if (!this.worlds.has(worldId)) {
|
||||||
|
throw new Error(`World ${worldId} does not exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wasmExports!.pinball_step_world(worldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建动态刚体
|
||||||
|
*/
|
||||||
|
async createDynamicBody(worldId: WorldId, position: Vector2, radius: number): Promise<BodyId> {
|
||||||
|
this.ensureLoaded();
|
||||||
|
|
||||||
|
if (!this.worlds.has(worldId)) {
|
||||||
|
throw new Error(`World ${worldId} does not exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyId = this.wasmExports!.pinball_create_dynamic_body(worldId, position.x, position.y);
|
||||||
|
|
||||||
|
// 记录刚体
|
||||||
|
const worldBodies = this.bodies.get(worldId)!;
|
||||||
|
worldBodies.set(bodyId, true);
|
||||||
|
|
||||||
|
console.log(`Created dynamic body ${bodyId} at (${position.x}, ${position.y}) with radius ${radius}`);
|
||||||
|
return bodyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建静态刚体(暂时使用动态刚体实现)
|
||||||
|
*/
|
||||||
|
async createStaticBody(worldId: WorldId, position: Vector2, radius: number): Promise<BodyId> {
|
||||||
|
// 目前 WASM 中只有 pinball_create_dynamic_body,后续可以扩展
|
||||||
|
return this.createDynamicBody(worldId, position, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁刚体(目前 WASM 中没有此函数,仅从记录中移除)
|
||||||
|
*/
|
||||||
|
async destroyBody(worldId: WorldId, bodyId: BodyId): Promise<void> {
|
||||||
|
const worldBodies = this.bodies.get(worldId);
|
||||||
|
if (worldBodies && worldBodies.has(bodyId)) {
|
||||||
|
worldBodies.delete(bodyId);
|
||||||
|
console.log(`Removed body ${bodyId} from tracking (WASM destroy not implemented yet)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取刚体位置
|
||||||
|
*/
|
||||||
|
async getBodyPosition(worldId: WorldId, bodyId: BodyId): Promise<Vector2 | null> {
|
||||||
|
this.ensureLoaded();
|
||||||
|
|
||||||
|
const worldBodies = this.bodies.get(worldId);
|
||||||
|
if (!worldBodies || !worldBodies.has(bodyId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = this.wasmExports!.pinball_get_body_x(worldId, bodyId);
|
||||||
|
const y = this.wasmExports!.pinball_get_body_y(worldId, bodyId);
|
||||||
|
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置刚体位置(目前 WASM 中没有此函数)
|
||||||
|
*/
|
||||||
|
async setBodyPosition(worldId: WorldId, bodyId: BodyId, position: Vector2): Promise<void> {
|
||||||
|
// 目前 WASM 中没有 pinball_set_body_position 函数
|
||||||
|
console.warn('setBodyPosition not implemented in WASM yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取刚体速度(目前 WASM 中没有此函数)
|
||||||
|
*/
|
||||||
|
async getBodyVelocity(worldId: WorldId, bodyId: BodyId): Promise<Vector2 | null> {
|
||||||
|
// 目前 WASM 中没有速度获取函数
|
||||||
|
console.warn('getBodyVelocity not implemented in WASM yet');
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置刚体速度(目前 WASM 中没有此函数)
|
||||||
|
*/
|
||||||
|
async setBodyVelocity(worldId: WorldId, bodyId: BodyId, velocity: Vector2): Promise<void> {
|
||||||
|
// 目前 WASM 中没有 pinball_set_body_velocity 函数
|
||||||
|
console.warn('setBodyVelocity not implemented in WASM yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有物理体数据
|
||||||
|
*/
|
||||||
|
async getAllBodies(worldId: WorldId): Promise<PhysicsBodyData[]> {
|
||||||
|
const worldBodies = this.bodies.get(worldId);
|
||||||
|
if (!worldBodies) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodiesData: PhysicsBodyData[] = [];
|
||||||
|
|
||||||
|
for (const bodyId of worldBodies.keys()) {
|
||||||
|
const position = await this.getBodyPosition(worldId, bodyId);
|
||||||
|
const velocity = await this.getBodyVelocity(worldId, bodyId);
|
||||||
|
|
||||||
|
if (position && velocity) {
|
||||||
|
bodiesData.push({
|
||||||
|
id: bodyId,
|
||||||
|
position,
|
||||||
|
velocity,
|
||||||
|
rotation: 0,
|
||||||
|
angularVelocity: 0,
|
||||||
|
bodyType: 'circle',
|
||||||
|
radius: 0.5, // 默认半径,可以后续改进
|
||||||
|
isStatic: false // 可以根据需要区分静态和动态
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bodiesData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建圆形刚体 (新接口方法)
|
||||||
|
*/
|
||||||
|
createCircle(options: import('../Core/IPhysicsEngine').CreateCircleOptions): BodyId {
|
||||||
|
this.ensureLoaded();
|
||||||
|
|
||||||
|
// 由于目前使用第一个世界,获取第一个世界ID
|
||||||
|
const firstWorldId = this.worlds.keys().next().value;
|
||||||
|
if (firstWorldId === undefined) {
|
||||||
|
throw new Error('No physics world created');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目前WASM只支持动态刚体创建
|
||||||
|
const bodyId = this.wasmExports!.pinball_create_dynamic_body(firstWorldId, options.position.x, options.position.y);
|
||||||
|
|
||||||
|
this.bodies.get(firstWorldId)!.set(bodyId, true);
|
||||||
|
return bodyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建矩形刚体 (新接口方法)
|
||||||
|
*/
|
||||||
|
createBox(options: import('../Core/IPhysicsEngine').CreateBoxOptions): BodyId {
|
||||||
|
this.ensureLoaded();
|
||||||
|
|
||||||
|
const firstWorldId = this.worlds.keys().next().value;
|
||||||
|
if (firstWorldId === undefined) {
|
||||||
|
throw new Error('No physics world created');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目前WASM只支持动态刚体创建,矩形用动态刚体模拟
|
||||||
|
const bodyId = this.wasmExports!.pinball_create_dynamic_body(firstWorldId, options.position.x, options.position.y);
|
||||||
|
|
||||||
|
this.bodies.get(firstWorldId)!.set(bodyId, true);
|
||||||
|
return bodyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除刚体 (新接口方法)
|
||||||
|
*/
|
||||||
|
removeBody(bodyId: BodyId): void {
|
||||||
|
// 查找包含此bodyId的世界
|
||||||
|
for (const [worldId, worldBodies] of this.bodies) {
|
||||||
|
if (worldBodies.has(bodyId)) {
|
||||||
|
worldBodies.delete(bodyId);
|
||||||
|
// 注意:这里需要WASM支持删除函数
|
||||||
|
// this.wasmExports!.pinball_remove_body(worldId, bodyId);
|
||||||
|
console.log(`Body ${bodyId} removed from world ${worldId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取刚体数据 (新接口方法)
|
||||||
|
*/
|
||||||
|
getBodyData(bodyId: BodyId): PhysicsBodyData | null {
|
||||||
|
// 查找包含此bodyId的世界
|
||||||
|
for (const [worldId, worldBodies] of this.bodies) {
|
||||||
|
if (worldBodies.has(bodyId)) {
|
||||||
|
// 这里需要同步调用WASM函数获取数据
|
||||||
|
try {
|
||||||
|
const x = this.wasmExports!.pinball_get_body_x(worldId, bodyId);
|
||||||
|
const y = this.wasmExports!.pinball_get_body_y(worldId, bodyId);
|
||||||
|
// WASM暂时不支持速度获取,使用默认值
|
||||||
|
const vx = 0;
|
||||||
|
const vy = 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: bodyId,
|
||||||
|
position: { x, y },
|
||||||
|
velocity: { x: vx, y: vy },
|
||||||
|
rotation: 0, // 暂时固定值
|
||||||
|
angularVelocity: 0, // 暂时固定值
|
||||||
|
bodyType: 'circle',
|
||||||
|
radius: 0.5, // 暂时固定值
|
||||||
|
isStatic: false // 暂时固定值
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get body data for body ${bodyId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源 (新接口方法)
|
||||||
|
*/
|
||||||
|
cleanup(): void {
|
||||||
|
this.dispose(); // 异步转同步
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源
|
||||||
|
*/
|
||||||
|
async dispose(): Promise<void> {
|
||||||
|
this.worlds.clear();
|
||||||
|
this.bodies.clear();
|
||||||
|
this.wasmInstance = null;
|
||||||
|
this.wasmExports = null;
|
||||||
|
this.wasmModule = null;
|
||||||
|
this.state = WasmModuleState.UNLOADED;
|
||||||
|
|
||||||
|
console.log('WASM Physics Engine disposed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前状态
|
||||||
|
*/
|
||||||
|
getState(): WasmModuleState {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查平台是否支持WASM(使用WasmLoader)
|
||||||
|
*/
|
||||||
|
isWasmSupported(): boolean {
|
||||||
|
return wasmLoader.isWasmSupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推荐的加载策略
|
||||||
|
*/
|
||||||
|
getRecommendedStrategy(): 'wasm' | 'asm' | 'unsupported' {
|
||||||
|
return wasmLoader.getRecommendedStrategy();
|
||||||
|
}
|
||||||
|
}
|
||||||
400
client-cocos/assets/scripts/Modules/Pinball/PinballManager.ts
Normal file
400
client-cocos/assets/scripts/Modules/Pinball/PinballManager.ts
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
/**
|
||||||
|
* PinballManager - Pinball模块主控制器
|
||||||
|
* 负责整个Pinball模块的生命周期管理和模式切换
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { _decorator, Camera, Component, director, Director, game, Game, Node, Scene } from 'cc';
|
||||||
|
import { EventBus } from './Core/EventBus';
|
||||||
|
import { GameState, PinballConfig } from './Core/GameData';
|
||||||
|
import { StandaloneMode } from './GameModes/StandaloneMode';
|
||||||
|
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
export enum PinballMode {
|
||||||
|
STANDALONE = 'standalone',
|
||||||
|
CLIENT_MULTIPLAYER = 'client-multiplayer',
|
||||||
|
SERVER_MULTIPLAYER = 'server-multiplayer'
|
||||||
|
}
|
||||||
|
|
||||||
|
@ccclass('PinballManager')
|
||||||
|
export class PinballManager extends Component {
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Node,
|
||||||
|
tooltip: "主相机节点,用于渲染游戏画面"
|
||||||
|
})
|
||||||
|
cameraNode: Node = null;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Node,
|
||||||
|
tooltip: "渲染容器节点,所有游戏对象将在此节点下渲染"
|
||||||
|
})
|
||||||
|
renderContainer: Node = null;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Node,
|
||||||
|
tooltip: "UI容器节点,用于显示游戏UI界面"
|
||||||
|
})
|
||||||
|
uiContainer: Node = null;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
tooltip: "启动时的默认游戏模式"
|
||||||
|
})
|
||||||
|
defaultMode: PinballMode = PinballMode.STANDALONE;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
tooltip: "是否在启动时自动开始游戏"
|
||||||
|
})
|
||||||
|
autoStart: boolean = true;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
tooltip: "调试模式,启用详细日志"
|
||||||
|
})
|
||||||
|
debugMode: boolean = true;
|
||||||
|
|
||||||
|
// 核心系统
|
||||||
|
private eventBus: EventBus = null;
|
||||||
|
private currentMode: PinballMode = null;
|
||||||
|
private currentGameMode: Component = null;
|
||||||
|
private mainCamera: Camera = null;
|
||||||
|
|
||||||
|
// 游戏状态
|
||||||
|
private isInitialized: boolean = false;
|
||||||
|
private isPaused: boolean = false;
|
||||||
|
private gameConfig: PinballConfig = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动 PinballManager
|
||||||
|
* 必须在配置应用后调用
|
||||||
|
*/
|
||||||
|
async Start(): Promise<boolean> {
|
||||||
|
this.log('[PinballManager] Start 开始');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 初始化事件总线
|
||||||
|
this.eventBus = EventBus.getInstance();
|
||||||
|
|
||||||
|
// 验证必需的节点
|
||||||
|
if (!this.validateRequiredNodes()) {
|
||||||
|
console.error('[PinballManager] 必需节点验证失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取主相机
|
||||||
|
this.initializeCamera();
|
||||||
|
|
||||||
|
// 创建默认配置
|
||||||
|
this.createDefaultConfig();
|
||||||
|
|
||||||
|
// 注册全局事件监听器
|
||||||
|
this.registerGlobalEvents();
|
||||||
|
|
||||||
|
// 初始化完成
|
||||||
|
this.isInitialized = true;
|
||||||
|
this.log('[PinballManager] 初始化完成');
|
||||||
|
|
||||||
|
this.log('[PinballManager] Start 完成');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[PinballManager] Start 过程中发生错误:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止 PinballManager
|
||||||
|
*/
|
||||||
|
Stop(): void {
|
||||||
|
this.log('[PinballManager] Stop 开始');
|
||||||
|
this.cleanup();
|
||||||
|
this.isInitialized = false;
|
||||||
|
this.log('[PinballManager] Stop 完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留 onDestroy 作为安全清理
|
||||||
|
onDestroy() {
|
||||||
|
this.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证必需的节点
|
||||||
|
*/
|
||||||
|
private validateRequiredNodes(): boolean {
|
||||||
|
if (!this.cameraNode) {
|
||||||
|
console.error('[PinballManager] cameraNode 未设置');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.renderContainer) {
|
||||||
|
console.error('[PinballManager] renderContainer 未设置');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化相机
|
||||||
|
*/
|
||||||
|
private initializeCamera(): void {
|
||||||
|
this.mainCamera = this.cameraNode.getComponent(Camera);
|
||||||
|
if (!this.mainCamera) {
|
||||||
|
console.error('[PinballManager] 主相机组件未找到');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('主相机初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认配置
|
||||||
|
*/
|
||||||
|
private createDefaultConfig(): void {
|
||||||
|
this.gameConfig = {
|
||||||
|
mode: this.defaultMode,
|
||||||
|
physicsSettings: {
|
||||||
|
gravity: { x: 0, y: -9.81 },
|
||||||
|
timeStep: 1 / 60
|
||||||
|
},
|
||||||
|
renderSettings: {
|
||||||
|
enableEffects: true,
|
||||||
|
maxParticles: 500
|
||||||
|
},
|
||||||
|
wasmPath: 'assets/wasm/pinball_physics.wasm'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.log('默认配置创建完成', this.gameConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册全局事件监听器
|
||||||
|
*/
|
||||||
|
private registerGlobalEvents(): void {
|
||||||
|
// 监听场景切换事件 - Cocos Creator 3.x 中使用静态常量
|
||||||
|
director.on(Director.EVENT_BEFORE_SCENE_LAUNCH, this.onSceneChange, this);
|
||||||
|
|
||||||
|
// 监听游戏暂停/恢复 - Cocos Creator 3.x 中使用game事件
|
||||||
|
game.on(Game.EVENT_HIDE, this.onGamePause, this);
|
||||||
|
game.on(Game.EVENT_SHOW, this.onGameResume, this);
|
||||||
|
|
||||||
|
this.log('全局事件监听器注册完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动游戏
|
||||||
|
*/
|
||||||
|
public async startGame(mode: PinballMode): Promise<boolean> {
|
||||||
|
if (!this.isInitialized) {
|
||||||
|
console.error('[PinballManager] 尚未初始化');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentGameMode) {
|
||||||
|
await this.stopCurrentGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(`开始启动游戏模式: ${mode}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (mode) {
|
||||||
|
case PinballMode.STANDALONE:
|
||||||
|
await this.startStandaloneMode();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PinballMode.CLIENT_MULTIPLAYER:
|
||||||
|
console.warn('[PinballManager] Client Multiplayer 模式尚未实现');
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case PinballMode.SERVER_MULTIPLAYER:
|
||||||
|
console.warn('[PinballManager] Server Multiplayer 模式尚未实现');
|
||||||
|
return false;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.error(`[PinballManager] 未知游戏模式: ${mode}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentMode = mode;
|
||||||
|
this.gameConfig.mode = mode;
|
||||||
|
|
||||||
|
this.log(`游戏模式 ${mode} 启动成功`);
|
||||||
|
|
||||||
|
// 发送游戏开始事件
|
||||||
|
this.eventBus.emit('game.started', { mode });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[PinballManager] 启动游戏模式 ${mode} 失败:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动Standalone模式
|
||||||
|
*/
|
||||||
|
private async startStandaloneMode(): Promise<void> {
|
||||||
|
// 添加StandaloneMode组件
|
||||||
|
const standaloneMode = this.node.addComponent(StandaloneMode);
|
||||||
|
|
||||||
|
// 设置所需的引用
|
||||||
|
standaloneMode.gameCamera = this.mainCamera;
|
||||||
|
standaloneMode.renderNode = this.renderContainer;
|
||||||
|
standaloneMode.boundsNode = this.renderContainer; // 简化设置
|
||||||
|
|
||||||
|
// StandaloneMode 会在 onLoad 中自动初始化
|
||||||
|
this.currentGameMode = standaloneMode;
|
||||||
|
|
||||||
|
this.log('Standalone模式启动完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止当前游戏模式
|
||||||
|
*/
|
||||||
|
public async stopCurrentGame(): Promise<void> {
|
||||||
|
if (!this.currentGameMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(`停止当前游戏模式: ${this.currentMode}`);
|
||||||
|
|
||||||
|
// 销毁当前游戏模式组件
|
||||||
|
this.currentGameMode.destroy();
|
||||||
|
this.currentGameMode = null;
|
||||||
|
this.currentMode = null;
|
||||||
|
|
||||||
|
// 发送游戏停止事件
|
||||||
|
this.eventBus.emit('game.stopped', {});
|
||||||
|
|
||||||
|
this.log('当前游戏模式已停止');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停游戏
|
||||||
|
*/
|
||||||
|
public pauseGame(): void {
|
||||||
|
if (this.isPaused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isPaused = true;
|
||||||
|
this.eventBus.emit('game.paused', {});
|
||||||
|
this.log('游戏已暂停');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复游戏
|
||||||
|
*/
|
||||||
|
public resumeGame(): void {
|
||||||
|
if (!this.isPaused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isPaused = false;
|
||||||
|
this.eventBus.emit('game.resumed', {});
|
||||||
|
this.log('游戏已恢复');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启当前游戏
|
||||||
|
*/
|
||||||
|
public async restartGame(): Promise<boolean> {
|
||||||
|
const currentMode = this.currentMode;
|
||||||
|
|
||||||
|
if (currentMode) {
|
||||||
|
await this.stopCurrentGame();
|
||||||
|
return await this.startGame(currentMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换游戏模式
|
||||||
|
*/
|
||||||
|
public async switchMode(newMode: PinballMode): Promise<boolean> {
|
||||||
|
if (this.currentMode === newMode) {
|
||||||
|
this.log(`已经是 ${newMode} 模式`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.startGame(newMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景切换事件处理
|
||||||
|
*/
|
||||||
|
private onSceneChange(scene: Scene): void {
|
||||||
|
this.log('场景切换事件');
|
||||||
|
// 可以在此处理场景切换时的清理工作
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏暂停事件处理
|
||||||
|
*/
|
||||||
|
private onGamePause(): void {
|
||||||
|
this.pauseGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏恢复事件处理
|
||||||
|
*/
|
||||||
|
private onGameResume(): void {
|
||||||
|
this.resumeGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源
|
||||||
|
*/
|
||||||
|
private cleanup(): void {
|
||||||
|
// 清理全局事件监听器
|
||||||
|
director.off(Director.EVENT_BEFORE_SCENE_LAUNCH, this.onSceneChange, this);
|
||||||
|
game.off(Game.EVENT_HIDE, this.onGamePause, this);
|
||||||
|
game.off(Game.EVENT_SHOW, this.onGameResume, this);
|
||||||
|
|
||||||
|
// 停止当前游戏
|
||||||
|
if (this.currentGameMode) {
|
||||||
|
this.stopCurrentGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('PinballManager 资源清理完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前游戏状态
|
||||||
|
*/
|
||||||
|
public getGameState(): GameState | null {
|
||||||
|
if (this.currentGameMode && this.currentGameMode instanceof StandaloneMode) {
|
||||||
|
return this.currentGameMode.getGameStats() as any;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取游戏配置
|
||||||
|
*/
|
||||||
|
public getConfig(): PinballConfig {
|
||||||
|
return this.gameConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新游戏配置
|
||||||
|
*/
|
||||||
|
public updateConfig(newConfig: Partial<PinballConfig>): void {
|
||||||
|
this.gameConfig = { ...this.gameConfig, ...newConfig };
|
||||||
|
this.log('游戏配置已更新', this.gameConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调试日志输出
|
||||||
|
*/
|
||||||
|
private log(message: string, data?: any): void {
|
||||||
|
if (this.debugMode) {
|
||||||
|
if (data) {
|
||||||
|
console.log(`[PinballManager] ${message}`, data);
|
||||||
|
} else {
|
||||||
|
console.log(`[PinballManager] ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "03de2ced-4b54-4dac-8773-b078be08a33f",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "27270c02-6c7f-448a-922d-12d7734844de",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
/**
|
||||||
|
* Pinball 渲染器实现
|
||||||
|
* 负责渲染物理对象、粒子效果等
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { _decorator, Camera, Color, Component, Graphics, Node, Prefab, Sprite, tween, Vec3 } from 'cc';
|
||||||
|
import { PhysicsBodyData, Vector2 } from '../Core/GameData';
|
||||||
|
import { IRenderer, ParticleEffect, RenderObject } from '../Core/IRenderer';
|
||||||
|
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
|
||||||
|
@ccclass
|
||||||
|
export class PinballRenderer extends Component implements IRenderer {
|
||||||
|
|
||||||
|
@property(Node)
|
||||||
|
private renderContainer: Node = null!;
|
||||||
|
|
||||||
|
@property(Prefab)
|
||||||
|
private ballPrefab: Prefab = null!;
|
||||||
|
|
||||||
|
private renderObjects: Map<string, Node> = new Map();
|
||||||
|
private particlePool: Node[] = [];
|
||||||
|
private camera: Camera = null!;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置相机
|
||||||
|
*/
|
||||||
|
setCamera(camera: Camera): void {
|
||||||
|
this.camera = camera;
|
||||||
|
console.log('Camera set for PinballRenderer');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置世界边界
|
||||||
|
*/
|
||||||
|
setWorldBounds(width: number, height: number): void {
|
||||||
|
console.log(`World bounds set to ${width} x ${height}`);
|
||||||
|
// 可以在此处设置渲染边界或背景
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化渲染器
|
||||||
|
*/
|
||||||
|
async initialize(parentNode: any): Promise<void> {
|
||||||
|
console.log('Initializing PinballRenderer...');
|
||||||
|
|
||||||
|
// 设置渲染容器
|
||||||
|
if (parentNode) {
|
||||||
|
this.renderContainer = parentNode;
|
||||||
|
} else {
|
||||||
|
this.renderContainer = this.node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取相机 - 在 Cocos Creator 3.x 中需要通过场景查找
|
||||||
|
const cameraNode = this.renderContainer.scene?.getChildByName('Main Camera');
|
||||||
|
if (cameraNode) {
|
||||||
|
this.camera = cameraNode.getComponent(Camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.camera) {
|
||||||
|
console.warn('Main camera not found, creating default camera');
|
||||||
|
const cameraNode = new Node('PinballCamera');
|
||||||
|
this.camera = cameraNode.addComponent(Camera);
|
||||||
|
this.renderContainer.addChild(cameraNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建默认球体预制件(如果没有提供)
|
||||||
|
if (!this.ballPrefab) {
|
||||||
|
await this.createDefaultBallPrefab();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('PinballRenderer initialized successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认的球体预制件
|
||||||
|
*/
|
||||||
|
private async createDefaultBallPrefab(): Promise<void> {
|
||||||
|
// 创建一个简单的圆形节点作为默认球体
|
||||||
|
const ballNode = new Node('DefaultBall');
|
||||||
|
|
||||||
|
// 添加 Sprite 组件
|
||||||
|
const sprite = ballNode.addComponent(Sprite);
|
||||||
|
|
||||||
|
// 创建圆形材质
|
||||||
|
const graphics = ballNode.addComponent(Graphics);
|
||||||
|
graphics.fillColor = Color.WHITE;
|
||||||
|
graphics.circle(0, 0, 25); // 半径 25 像素
|
||||||
|
graphics.fill();
|
||||||
|
|
||||||
|
console.log('Created default ball prefab');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染物理体
|
||||||
|
*/
|
||||||
|
renderBodies(bodies: PhysicsBodyData[]): void {
|
||||||
|
// 清理不存在的渲染对象
|
||||||
|
const currentBodyIds = new Set(bodies.map(body => body.id.toString()));
|
||||||
|
for (const [id, node] of this.renderObjects) {
|
||||||
|
if (!currentBodyIds.has(id)) {
|
||||||
|
this.removeRenderObject(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新或创建渲染对象
|
||||||
|
for (const body of bodies) {
|
||||||
|
const bodyIdStr = body.id.toString();
|
||||||
|
let renderNode = this.renderObjects.get(bodyIdStr);
|
||||||
|
|
||||||
|
if (!renderNode) {
|
||||||
|
// 创建新的渲染对象
|
||||||
|
const renderObject = this.createRenderObject(body);
|
||||||
|
renderNode = this.createRenderNode(renderObject);
|
||||||
|
this.renderObjects.set(bodyIdStr, renderNode);
|
||||||
|
this.renderContainer.addChild(renderNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新位置
|
||||||
|
renderNode.setPosition(body.position.x * 100, body.position.y * 100); // 放大显示
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建渲染对象数据
|
||||||
|
*/
|
||||||
|
createRenderObject(body: PhysicsBodyData): RenderObject {
|
||||||
|
return {
|
||||||
|
id: body.id.toString(),
|
||||||
|
position: body.position,
|
||||||
|
radius: body.radius,
|
||||||
|
color: body.isStatic ?
|
||||||
|
{ r: 128, g: 128, b: 128, a: 255 } : // 静态物体:灰色
|
||||||
|
{ r: 255, g: 100, b: 100, a: 255 }, // 动态物体:红色
|
||||||
|
layer: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建渲染节点
|
||||||
|
*/
|
||||||
|
private createRenderNode(renderObject: RenderObject): Node {
|
||||||
|
const node = new Node(`Ball_${renderObject.id}`);
|
||||||
|
|
||||||
|
// 添加 Graphics 组件用于绘制圆形
|
||||||
|
const graphics = node.addComponent(Graphics);
|
||||||
|
graphics.fillColor = new Color(
|
||||||
|
renderObject.color.r,
|
||||||
|
renderObject.color.g,
|
||||||
|
renderObject.color.b,
|
||||||
|
renderObject.color.a
|
||||||
|
);
|
||||||
|
|
||||||
|
const radius = Math.max(renderObject.radius * 100, 10); // 最小半径 10 像素
|
||||||
|
graphics.circle(0, 0, radius);
|
||||||
|
graphics.fill();
|
||||||
|
|
||||||
|
// 添加边框
|
||||||
|
graphics.strokeColor = Color.BLACK;
|
||||||
|
graphics.lineWidth = 2;
|
||||||
|
graphics.circle(0, 0, radius);
|
||||||
|
graphics.stroke();
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新渲染对象
|
||||||
|
*/
|
||||||
|
updateRenderObject(renderObject: RenderObject, body: PhysicsBodyData): void {
|
||||||
|
const node = this.renderObjects.get(renderObject.id);
|
||||||
|
if (node) {
|
||||||
|
// 更新位置
|
||||||
|
node.setPosition(body.position.x * 100, body.position.y * 100);
|
||||||
|
|
||||||
|
// 可以在此添加更多属性的更新,如颜色、大小等
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除渲染对象
|
||||||
|
*/
|
||||||
|
removeRenderObject(bodyId: string): void {
|
||||||
|
const node = this.renderObjects.get(bodyId);
|
||||||
|
if (node) {
|
||||||
|
node.removeFromParent();
|
||||||
|
this.renderObjects.delete(bodyId);
|
||||||
|
console.log(`Removed render object ${bodyId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放粒子效果
|
||||||
|
*/
|
||||||
|
playParticleEffect(effect: ParticleEffect): void {
|
||||||
|
// 创建简单的粒子效果节点
|
||||||
|
const particleNode = this.getParticleNode();
|
||||||
|
particleNode.setPosition(effect.position.x * 100, effect.position.y * 100);
|
||||||
|
|
||||||
|
// 使用 Cocos Creator 3.x 的 tween 系统
|
||||||
|
tween(particleNode)
|
||||||
|
.parallel(
|
||||||
|
tween().to(effect.lifetime, { opacity: 0 }),
|
||||||
|
tween().by(effect.lifetime, {
|
||||||
|
position: new Vec3(effect.velocity.x * 50, effect.velocity.y * 50, 0)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.call(() => {
|
||||||
|
this.recycleParticleNode(particleNode);
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
|
||||||
|
this.renderContainer.addChild(particleNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取粒子节点(对象池)
|
||||||
|
*/
|
||||||
|
private getParticleNode(): Node {
|
||||||
|
if (this.particlePool.length > 0) {
|
||||||
|
return this.particlePool.pop()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = new Node('Particle');
|
||||||
|
const graphics = node.addComponent(Graphics);
|
||||||
|
graphics.fillColor = Color.YELLOW;
|
||||||
|
graphics.circle(0, 0, 3);
|
||||||
|
graphics.fill();
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回收粒子节点
|
||||||
|
*/
|
||||||
|
private recycleParticleNode(node: Node): void {
|
||||||
|
node.removeFromParent();
|
||||||
|
// 重置透明度 - 在 Cocos Creator 3.x 中通过 UIOpacity 组件
|
||||||
|
const uiOpacity = node.getComponent('cc.UIOpacity') as any;
|
||||||
|
if (uiOpacity) {
|
||||||
|
uiOpacity.opacity = 255;
|
||||||
|
}
|
||||||
|
node.setPosition(0, 0, 0); // 重置位置
|
||||||
|
this.particlePool.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有渲染对象
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
for (const [id, node] of this.renderObjects) {
|
||||||
|
node.removeFromParent();
|
||||||
|
}
|
||||||
|
this.renderObjects.clear();
|
||||||
|
|
||||||
|
// 清理粒子池
|
||||||
|
for (const particle of this.particlePool) {
|
||||||
|
particle.removeFromParent();
|
||||||
|
}
|
||||||
|
this.particlePool = [];
|
||||||
|
|
||||||
|
console.log('Cleared all render objects');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置相机位置
|
||||||
|
*/
|
||||||
|
setCameraPosition(position: Vector2): void {
|
||||||
|
if (this.camera) {
|
||||||
|
this.camera.node.setPosition(position.x * 100, position.y * 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置相机缩放
|
||||||
|
*/
|
||||||
|
setCameraZoom(zoom: number): void {
|
||||||
|
if (this.camera) {
|
||||||
|
// 在 Cocos Creator 3.x 中使用 orthoHeight 控制缩放
|
||||||
|
this.camera.orthoHeight = 600 / zoom; // 基础高度 600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁渲染器
|
||||||
|
*/
|
||||||
|
dispose(): void {
|
||||||
|
this.clear();
|
||||||
|
console.log('PinballRenderer disposed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "da8038e2-7c90-4e3e-a3f6-4f8b88b3a11d",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
9
client-cocos/assets/scripts/Utils.meta
Normal file
9
client-cocos/assets/scripts/Utils.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "8d22d76a-17c1-48bd-a151-d2ed14a97029",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
321
client-cocos/assets/scripts/Utils/WasmLoader.ts
Normal file
321
client-cocos/assets/scripts/Utils/WasmLoader.ts
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
/**
|
||||||
|
* WASM加载器
|
||||||
|
* 基于Cocos Creator官方文档的WASM/ASM加载最佳实践
|
||||||
|
* https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/wasm-asm-load.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Asset, assetManager, sys } from 'cc';
|
||||||
|
import { EDITOR } from 'cc/env';
|
||||||
|
|
||||||
|
// ASM模块内存配置常量
|
||||||
|
const PAGESIZE = 65536; // 64KiB
|
||||||
|
const PAGECOUNT = 32 * 16; // 可根据需要调整
|
||||||
|
const MEMORYSIZE = PAGESIZE * PAGECOUNT; // 32 MiB
|
||||||
|
|
||||||
|
|
||||||
|
export interface WasmLoadOptions {
|
||||||
|
bundleName?: string;
|
||||||
|
fileName: string;
|
||||||
|
editorUuid?: string; // 编辑器内的资源UUID
|
||||||
|
supportAsm?: boolean; // 是否支持ASM回退
|
||||||
|
asmFileName?: string; // ASM文件名
|
||||||
|
asmEditorUuid?: string; // ASM文件的编辑器UUID
|
||||||
|
wasmFactory?: any; // WASM工厂函数
|
||||||
|
asmFactory?: any; // ASM工厂函数
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WasmLoadResult {
|
||||||
|
instance: any;
|
||||||
|
isWasm: boolean; // true为WASM,false为ASM
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WasmLoader {
|
||||||
|
private static instance: WasmLoader;
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
|
private constructor() { }
|
||||||
|
|
||||||
|
public static getInstance(): WasmLoader {
|
||||||
|
if (!WasmLoader.instance) {
|
||||||
|
WasmLoader.instance = new WasmLoader();
|
||||||
|
}
|
||||||
|
return WasmLoader.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化WASM加载器
|
||||||
|
* 注册WASM/ASM文件的下载器和解析器
|
||||||
|
*/
|
||||||
|
public initialize(): void {
|
||||||
|
if (this.initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断当前平台是否支持加载WASM文件
|
||||||
|
// Cocos引擎目前暂不支持在iOS平台加载WASM文件
|
||||||
|
if (sys.hasFeature(sys.Feature.WASM) || (sys.isNative && sys.os !== sys.OS.IOS)) {
|
||||||
|
if (sys.isNative) {
|
||||||
|
// 原生平台
|
||||||
|
//@ts-ignore
|
||||||
|
assetManager.downloader.register('.wasm', assetManager.downloader._downloaders[".bin"]);
|
||||||
|
//@ts-ignore
|
||||||
|
assetManager.parser.register('.wasm', assetManager.parser._parsers[".bin"]);
|
||||||
|
} else if (sys.isBrowser || sys.platform === sys.Platform.WECHAT_GAME) {
|
||||||
|
// 浏览器或微信小游戏平台
|
||||||
|
//@ts-ignore
|
||||||
|
assetManager.downloader.register('.wasm', assetManager.downloader._downloadArrayBuffer);
|
||||||
|
//@ts-ignore
|
||||||
|
assetManager.downloader.register('.mem', assetManager.downloader._downloadArrayBuffer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不支持WASM的平台,注册ASM相关文件
|
||||||
|
if (sys.isNative) {
|
||||||
|
//@ts-ignore
|
||||||
|
assetManager.downloader.register('.mem', assetManager.downloader._downloaders[".bin"]);
|
||||||
|
//@ts-ignore
|
||||||
|
assetManager.parser.register('.mem', assetManager.parser._parsers[".bin"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
console.log('[WasmLoader] 初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载WASM模块
|
||||||
|
* @param options 加载配置选项(必须包含工厂函数)
|
||||||
|
* @returns Promise<WasmLoadResult>
|
||||||
|
*/
|
||||||
|
public async loadWasmModule(options: WasmLoadOptions): Promise<WasmLoadResult> {
|
||||||
|
if (!this.initialized) {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否支持WASM
|
||||||
|
if (sys.hasFeature(sys.Feature.WASM) || (sys.isNative && sys.os !== sys.OS.IOS)) {
|
||||||
|
// 加载WASM
|
||||||
|
return this.loadWasm(options);
|
||||||
|
} else if (options.supportAsm && options.asmFileName && options.asmFactory) {
|
||||||
|
// 回退到ASM
|
||||||
|
return this.loadAsm(options);
|
||||||
|
} else {
|
||||||
|
throw new Error('当前平台不支持WASM,且未配置ASM回退选项');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载WASM
|
||||||
|
*/
|
||||||
|
private async loadWasm(options: WasmLoadOptions): Promise<WasmLoadResult> {
|
||||||
|
if (!options.wasmFactory) {
|
||||||
|
throw new Error('WASM工厂函数未提供');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wasmFactory = options.wasmFactory;
|
||||||
|
|
||||||
|
// 加载WASM二进制文件
|
||||||
|
const wasmFile = await this.loadWasmFile(
|
||||||
|
options.bundleName || '',
|
||||||
|
options.fileName,
|
||||||
|
options.editorUuid
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化WASM
|
||||||
|
const instance = await this.initWasm(wasmFactory, wasmFile);
|
||||||
|
|
||||||
|
return {
|
||||||
|
instance,
|
||||||
|
isWasm: true
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WasmLoader] WASM加载失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载ASM
|
||||||
|
*/
|
||||||
|
private async loadAsm(options: WasmLoadOptions): Promise<WasmLoadResult> {
|
||||||
|
if (!options.asmFactory) {
|
||||||
|
throw new Error('ASM工厂函数未提供');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const asmFactory = options.asmFactory;
|
||||||
|
|
||||||
|
// 加载ASM内存文件
|
||||||
|
const asmFile = await this.loadWasmFile(
|
||||||
|
options.bundleName || '',
|
||||||
|
options.asmFileName || options.fileName,
|
||||||
|
options.asmEditorUuid
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化ASM
|
||||||
|
const instance = await this.initAsm(asmFactory, asmFile);
|
||||||
|
|
||||||
|
return {
|
||||||
|
instance,
|
||||||
|
isWasm: false
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WasmLoader] ASM加载失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载WASM/ASM二进制文件
|
||||||
|
*/
|
||||||
|
private loadWasmFile(bundleName: string, fileName: string, editorUuid?: string): Promise<Asset> {
|
||||||
|
return new Promise<Asset>((resolve, reject) => {
|
||||||
|
if (EDITOR) {
|
||||||
|
// 编辑器内通过UUID加载资源
|
||||||
|
if (editorUuid) {
|
||||||
|
assetManager.loadAny(editorUuid, (err, file: Asset) => {
|
||||||
|
if (!err) {
|
||||||
|
resolve(file);
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Error('编辑器环境下需要提供editorUuid'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 运行时通过Bundle加载
|
||||||
|
if (bundleName && fileName) {
|
||||||
|
assetManager.loadBundle(bundleName, (err, bundle) => {
|
||||||
|
if (!err) {
|
||||||
|
bundle.load(fileName, Asset, (err2: any, file: Asset) => {
|
||||||
|
if (!err2) {
|
||||||
|
resolve(file);
|
||||||
|
} else {
|
||||||
|
reject(err2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Error('运行时环境下需要提供bundleName和fileName'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化WASM模块
|
||||||
|
*/
|
||||||
|
private initWasm(wasmFactory: any, wasmFile: Asset): Promise<any> {
|
||||||
|
const self = this;
|
||||||
|
return new Promise<any>((resolve, reject) => {
|
||||||
|
wasmFactory({
|
||||||
|
instantiateWasm(importObject: WebAssembly.Imports, receiveInstance: any) {
|
||||||
|
self.instantiateWasm(wasmFile, importObject).then((result) => {
|
||||||
|
receiveInstance(result.instance, result.module);
|
||||||
|
}).catch((err) => reject(err));
|
||||||
|
}
|
||||||
|
}).then((instance: any) => {
|
||||||
|
resolve(instance);
|
||||||
|
}).catch((err: any) => reject(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实例化WASM
|
||||||
|
*/
|
||||||
|
private instantiateWasm(wasmFile: Asset, importObject: WebAssembly.Imports): Promise<any> {
|
||||||
|
if (sys.isBrowser || sys.isNative) {
|
||||||
|
//@ts-ignore
|
||||||
|
return WebAssembly.instantiate(wasmFile._file, importObject);
|
||||||
|
} else if (sys.platform === sys.Platform.WECHAT_GAME) {
|
||||||
|
//@ts-ignore
|
||||||
|
return CCWebAssembly.instantiate(wasmFile.nativeUrl, importObject);
|
||||||
|
} else {
|
||||||
|
return Promise.reject(new Error('不支持的平台'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化ASM模块
|
||||||
|
*/
|
||||||
|
private initAsm(asmFactory: any, asmFile: Asset): Promise<any> {
|
||||||
|
const asmMemory: any = {};
|
||||||
|
asmMemory.buffer = new ArrayBuffer(MEMORYSIZE);
|
||||||
|
|
||||||
|
const module = {
|
||||||
|
asmMemory,
|
||||||
|
memoryInitializerRequest: {
|
||||||
|
//@ts-ignore
|
||||||
|
response: asmFile._file,
|
||||||
|
status: 200,
|
||||||
|
} as Partial<XMLHttpRequest>,
|
||||||
|
};
|
||||||
|
|
||||||
|
return asmFactory(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷方法:加载简单的WASM模块(仅WASM,不支持ASM回退)
|
||||||
|
*/
|
||||||
|
public async loadSimpleWasm(wasmFactory: any, fileName: string, editorUuid?: string, bundleName?: string): Promise<any> {
|
||||||
|
const result = await this.loadWasmModule({
|
||||||
|
fileName,
|
||||||
|
editorUuid,
|
||||||
|
bundleName,
|
||||||
|
supportAsm: false,
|
||||||
|
wasmFactory
|
||||||
|
});
|
||||||
|
return result.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷方法:加载支持ASM回退的WASM模块
|
||||||
|
*/
|
||||||
|
public async loadWasmWithAsmFallback(
|
||||||
|
wasmFactory: any,
|
||||||
|
asmFactory: any,
|
||||||
|
fileName: string,
|
||||||
|
asmFileName: string,
|
||||||
|
editorUuid?: string,
|
||||||
|
asmEditorUuid?: string,
|
||||||
|
bundleName?: string
|
||||||
|
): Promise<WasmLoadResult> {
|
||||||
|
return this.loadWasmModule({
|
||||||
|
fileName,
|
||||||
|
editorUuid,
|
||||||
|
bundleName,
|
||||||
|
supportAsm: true,
|
||||||
|
asmFileName,
|
||||||
|
asmEditorUuid,
|
||||||
|
wasmFactory,
|
||||||
|
asmFactory
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查当前平台是否支持WASM
|
||||||
|
*/
|
||||||
|
public isWasmSupported(): boolean {
|
||||||
|
return sys.hasFeature(sys.Feature.WASM) || (sys.isNative && sys.os !== sys.OS.IOS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推荐的加载策略
|
||||||
|
*/
|
||||||
|
public getRecommendedStrategy(): 'wasm' | 'asm' | 'unsupported' {
|
||||||
|
if (this.isWasmSupported()) {
|
||||||
|
return 'wasm';
|
||||||
|
} else if (sys.isNative || sys.isBrowser) {
|
||||||
|
return 'asm';
|
||||||
|
} else {
|
||||||
|
return 'unsupported';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例实例
|
||||||
|
export const wasmLoader = WasmLoader.getInstance();
|
||||||
9
client-cocos/assets/scripts/Utils/WasmLoader.ts.meta
Normal file
9
client-cocos/assets/scripts/Utils/WasmLoader.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "1a752554-ae7a-4ac3-b468-31ad25757d52",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
128
client-cocos/assets/scripts/Utils/WasmLoaderExample.ts
Normal file
128
client-cocos/assets/scripts/Utils/WasmLoaderExample.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* WasmLoader 使用示例
|
||||||
|
*
|
||||||
|
* 这个文件展示如何使用 WasmLoader 来加载 WASM 模块
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { wasmLoader } from './WasmLoader';
|
||||||
|
|
||||||
|
// 假设我们有一个 WASM 工厂函数(需要从 JS 胶水代码中导入)
|
||||||
|
// import wasmFactory from '../path/to/your-wasm-module.js';
|
||||||
|
|
||||||
|
export class WasmLoaderExample {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例1: 加载简单的 WASM 模块
|
||||||
|
*/
|
||||||
|
async loadSimpleWasmExample(wasmFactory: any) {
|
||||||
|
try {
|
||||||
|
// 首先需要初始化加载器(通常在应用启动时执行一次)
|
||||||
|
wasmLoader.initialize();
|
||||||
|
|
||||||
|
// 假设你有一个 WASM 工厂函数
|
||||||
|
// const wasmFactory = (await import('../wasm/pinball_physics.js')).default;
|
||||||
|
|
||||||
|
// 加载 WASM 模块
|
||||||
|
const instance = await wasmLoader.loadSimpleWasm(
|
||||||
|
wasmFactory, // WASM 工厂函数
|
||||||
|
'pinball_physics.wasm', // WASM 文件名
|
||||||
|
'44cacb3c-e901-455d-b3e1-1c38a69718e1', // 编辑器中的 UUID(可选)
|
||||||
|
'wasmFiles' // Bundle 名称(可选)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 现在可以使用 WASM 模块的函数
|
||||||
|
console.log('WASM 模块加载成功:', instance);
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WASM 加载失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例2: 加载支持 ASM 回退的 WASM 模块
|
||||||
|
*/
|
||||||
|
async loadWasmWithFallbackExample(wasmFactory: any, asmFactory: any) {
|
||||||
|
try {
|
||||||
|
wasmLoader.initialize();
|
||||||
|
|
||||||
|
// 假设你有 WASM 和 ASM 工厂函数
|
||||||
|
// const wasmFactory = (await import('../wasm/pinball_physics.js')).default;
|
||||||
|
// const asmFactory = (await import('../wasm/pinball_physics.asm.js')).default;
|
||||||
|
|
||||||
|
const result = await wasmLoader.loadWasmWithAsmFallback(
|
||||||
|
wasmFactory, // WASM 工厂函数
|
||||||
|
asmFactory, // ASM 工厂函数
|
||||||
|
'pinball_physics.wasm', // WASM 文件名
|
||||||
|
'pinball_physics.asm.mem', // ASM 内存文件名
|
||||||
|
'44cacb3c-e901-455d-b3e1-1c38a69718e1', // WASM UUID
|
||||||
|
'3400003e-dc3c-43c1-8757-3e082429125a', // ASM UUID
|
||||||
|
'wasmFiles' // Bundle 名称
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('模块加载成功,使用的是:', result.isWasm ? 'WASM' : 'ASM');
|
||||||
|
console.log('模块实例:', result.instance);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('模块加载失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例3: 检查平台支持情况
|
||||||
|
*/
|
||||||
|
checkPlatformSupport() {
|
||||||
|
const isWasmSupported = wasmLoader.isWasmSupported();
|
||||||
|
const strategy = wasmLoader.getRecommendedStrategy();
|
||||||
|
|
||||||
|
console.log('WASM 支持:', isWasmSupported);
|
||||||
|
console.log('推荐策略:', strategy);
|
||||||
|
|
||||||
|
return { isWasmSupported, strategy };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例4: 高级用法 - 自定义配置
|
||||||
|
*/
|
||||||
|
async loadCustomWasm(wasmFactory: any, asmFactory: any) {
|
||||||
|
try {
|
||||||
|
wasmLoader.initialize();
|
||||||
|
|
||||||
|
// 使用完整配置
|
||||||
|
const result = await wasmLoader.loadWasmModule({
|
||||||
|
fileName: 'pinball_physics.wasm',
|
||||||
|
bundleName: 'wasmFiles',
|
||||||
|
editorUuid: '44cacb3c-e901-455d-b3e1-1c38a69718e1',
|
||||||
|
supportAsm: true,
|
||||||
|
asmFileName: 'pinball_physics.asm.mem',
|
||||||
|
asmEditorUuid: '3400003e-dc3c-43c1-8757-3e082429125a',
|
||||||
|
wasmFactory: wasmFactory,
|
||||||
|
asmFactory: asmFactory
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('自定义 WASM 加载失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用说明:
|
||||||
|
*
|
||||||
|
* 1. 首先将 WASM 文件和对应的 JS 胶水代码放到项目中
|
||||||
|
* 2. 将 .wasm 文件导入到编辑器的资源管理器中
|
||||||
|
* 3. 在编辑器中获取 .wasm 文件的 UUID
|
||||||
|
* 4. 导入 JS 胶水代码并获取工厂函数
|
||||||
|
* 5. 调用 wasmLoader 的方法加载 WASM 模块
|
||||||
|
*
|
||||||
|
* 注意事项:
|
||||||
|
* - 在编辑器环境下需要提供资源的 UUID
|
||||||
|
* - 在运行时环境下需要提供 Bundle 名称和文件名
|
||||||
|
* - iOS 平台不支持 WASM,需要提供 ASM 回退
|
||||||
|
* - 确保在使用前调用 initialize() 方法
|
||||||
|
*/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.23",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "2ecfafcc-6d3c-443e-8e24-3207988f4742",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
11
client-cocos/assets/wasm.meta
Normal file
11
client-cocos/assets/wasm.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "812832c8-4a7e-482a-b607-a64b8f91ae2a",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {
|
||||||
|
"isBundle": true
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
client-cocos/assets/wasm/pinball_physics.wasm
(Stored with Git LFS)
Normal file
BIN
client-cocos/assets/wasm/pinball_physics.wasm
(Stored with Git LFS)
Normal file
Binary file not shown.
12
client-cocos/assets/wasm/pinball_physics.wasm.meta
Normal file
12
client-cocos/assets/wasm/pinball_physics.wasm.meta
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.0.1",
|
||||||
|
"importer": "*",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "401aec55-42ae-48c1-9160-22c930389b53",
|
||||||
|
"files": [
|
||||||
|
".json",
|
||||||
|
".wasm"
|
||||||
|
],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
193
client-cocos/build-physics.bat
Normal file
193
client-cocos/build-physics.bat
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo Pinball Physics 构建脚本
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: 设置构建模式,默认为 debug
|
||||||
|
set BUILD_MODE=%1
|
||||||
|
if "%BUILD_MODE%"=="" set BUILD_MODE=debug
|
||||||
|
|
||||||
|
:: 验证构建模式
|
||||||
|
if /i "%BUILD_MODE%" neq "debug" if /i "%BUILD_MODE%" neq "release" (
|
||||||
|
echo 错误: 无效的构建模式 "%BUILD_MODE%"
|
||||||
|
echo 支持的模式: debug ^| release
|
||||||
|
echo.
|
||||||
|
echo 用法: build-physics.bat [debug^|release]
|
||||||
|
echo 示例: build-physics.bat release
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
echo 构建模式: %BUILD_MODE%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: 设置路径变量
|
||||||
|
set SCRIPT_DIR=%~dp0
|
||||||
|
set PROJECT_ROOT=%SCRIPT_DIR%..\
|
||||||
|
set PHYSICS_DIR=%PROJECT_ROOT%pinball-physics
|
||||||
|
set TARGET_DIR=%PHYSICS_DIR%\target\wasm32-unknown-unknown\%BUILD_MODE%
|
||||||
|
set WASM_SOURCE=%TARGET_DIR%\pinball_physics.wasm
|
||||||
|
set WASM_OUTPUT=%SCRIPT_DIR%assets\wasm\pinball_physics.bin
|
||||||
|
set ASM_OUTPUT=%SCRIPT_DIR%assets\wasm\pinball_physics.js
|
||||||
|
set EMSCRIPTEN_TOOL=%PROJECT_ROOT%tools\emscripten\gen-asm.bat
|
||||||
|
|
||||||
|
echo 路径配置:
|
||||||
|
echo 物理引擎目录: %PHYSICS_DIR%
|
||||||
|
echo WASM 源文件: %WASM_SOURCE%
|
||||||
|
echo WASM 输出: %WASM_OUTPUT%
|
||||||
|
echo asm.js 输出: %ASM_OUTPUT%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: 检查物理引擎项目是否存在
|
||||||
|
if not exist "%PHYSICS_DIR%\Cargo.toml" (
|
||||||
|
echo 错误: 未找到物理引擎项目
|
||||||
|
echo 路径: %PHYSICS_DIR%
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 检查 Rust 是否安装
|
||||||
|
rustc --version >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo 错误: 未找到 Rust 编译器
|
||||||
|
echo 请先安装 Rust: https://rustup.rs/
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 检查 wasm32-unknown-unknown 目标是否安装
|
||||||
|
rustup target list --installed | findstr "wasm32-unknown-unknown" >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo 安装 wasm32-unknown-unknown 目标...
|
||||||
|
rustup target add wasm32-unknown-unknown
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo 错误: 安装 wasm32-unknown-unknown 目标失败
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 创建输出目录
|
||||||
|
if not exist "%SCRIPT_DIR%assets\wasm" (
|
||||||
|
echo 创建输出目录: assets\wasm
|
||||||
|
mkdir "%SCRIPT_DIR%assets\wasm"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ----------------------------------------
|
||||||
|
echo 第1步: 编译物理引擎 (模式: %BUILD_MODE%)
|
||||||
|
echo ----------------------------------------
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: 切换到物理引擎目录并编译
|
||||||
|
cd /d "%PHYSICS_DIR%"
|
||||||
|
|
||||||
|
if /i "%BUILD_MODE%"=="release" (
|
||||||
|
echo 执行命令: cargo build --target wasm32-unknown-unknown --release
|
||||||
|
cargo build --target wasm32-unknown-unknown --release
|
||||||
|
) else (
|
||||||
|
echo 执行命令: cargo build --target wasm32-unknown-unknown
|
||||||
|
cargo build --target wasm32-unknown-unknown
|
||||||
|
)
|
||||||
|
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo 错误: 物理引擎编译失败
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 检查编译输出是否存在
|
||||||
|
if not exist "%WASM_SOURCE%" (
|
||||||
|
echo 错误: 编译输出文件不存在
|
||||||
|
echo 期望位置: %WASM_SOURCE%
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 显示编译结果信息
|
||||||
|
for %%A in ("%WASM_SOURCE%") do (
|
||||||
|
set source_size=%%~zA
|
||||||
|
set /a size_kb=!source_size!/1024
|
||||||
|
echo 编译成功! 文件大小: !size_kb! KB
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ----------------------------------------
|
||||||
|
echo 第2步: 拷贝 WASM 文件
|
||||||
|
echo ----------------------------------------
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: 拷贝 WASM 文件到客户端资源目录
|
||||||
|
echo 拷贝: %WASM_SOURCE%
|
||||||
|
echo 到: %WASM_OUTPUT%
|
||||||
|
|
||||||
|
copy "%WASM_SOURCE%" "%WASM_OUTPUT%" >nul
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo 错误: 拷贝 WASM 文件失败
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
echo 拷贝成功!
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ----------------------------------------
|
||||||
|
echo 第3步: 生成 asm.js 文件
|
||||||
|
echo ----------------------------------------
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: 检查 Emscripten 转换工具是否存在
|
||||||
|
if not exist "%EMSCRIPTEN_TOOL%" (
|
||||||
|
echo 错误: 未找到 Emscripten 转换工具
|
||||||
|
echo 路径: %EMSCRIPTEN_TOOL%
|
||||||
|
echo 请先运行: tools\emscripten\install.bat
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 切换回项目根目录
|
||||||
|
cd /d "%PROJECT_ROOT%"
|
||||||
|
|
||||||
|
:: 执行 WASM 到 asm.js 转换
|
||||||
|
echo 执行转换: WASM → asm.js
|
||||||
|
echo 命令: %EMSCRIPTEN_TOOL% "%WASM_SOURCE%" "%ASM_OUTPUT%"
|
||||||
|
echo.
|
||||||
|
|
||||||
|
call "%EMSCRIPTEN_TOOL%" "%WASM_SOURCE%" "%ASM_OUTPUT%"
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo 错误: asm.js 转换失败
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo 构建完成!
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
echo 输出文件:
|
||||||
|
echo WASM: %WASM_OUTPUT%
|
||||||
|
if exist "%ASM_OUTPUT%" (
|
||||||
|
for %%A in ("%WASM_OUTPUT%") do (
|
||||||
|
set wasm_size=%%~zA
|
||||||
|
set /a wasm_kb=!wasm_size!/1024
|
||||||
|
)
|
||||||
|
for %%A in ("%ASM_OUTPUT%") do (
|
||||||
|
set asm_size=%%~zA
|
||||||
|
set /a asm_kb=!asm_size!/1024
|
||||||
|
)
|
||||||
|
echo asm.js: %ASM_OUTPUT%
|
||||||
|
echo.
|
||||||
|
echo 文件大小:
|
||||||
|
echo WASM: !wasm_kb! KB
|
||||||
|
echo asm.js: !asm_kb! KB
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
echo 构建模式: %BUILD_MODE%
|
||||||
|
echo 可以在 Cocos Creator 中使用这些文件了!
|
||||||
|
|
||||||
|
goto :end
|
||||||
|
|
||||||
|
:error
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo 构建失败!
|
||||||
|
echo ========================================
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:end
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
7
client-cocos/package.json
Normal file
7
client-cocos/package.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "client-cocos",
|
||||||
|
"uuid": "20f4ca2c-2427-4801-805a-4b81e119fc54",
|
||||||
|
"creator": {
|
||||||
|
"version": "3.8.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
client-cocos/settings/v2/packages/builder.json
Normal file
3
client-cocos/settings/v2/packages/builder.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__version__": "1.3.7"
|
||||||
|
}
|
||||||
3
client-cocos/settings/v2/packages/device.json
Normal file
3
client-cocos/settings/v2/packages/device.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__version__": "1.0.1"
|
||||||
|
}
|
||||||
150
client-cocos/settings/v2/packages/engine.json
Normal file
150
client-cocos/settings/v2/packages/engine.json
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
{
|
||||||
|
"__version__": "1.0.7",
|
||||||
|
"modules": {
|
||||||
|
"cache": {
|
||||||
|
"base": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"graphcis": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"gfx-webgl": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"gfx-webgl2": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"animation": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"skeletal-animation": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"3d": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"2d": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"xr": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"particle": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"physics": {
|
||||||
|
"_value": false,
|
||||||
|
"_option": "physics-ammo"
|
||||||
|
},
|
||||||
|
"physics-ammo": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"physics-cannon": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"physics-physx": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"physics-builtin": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"physics-2d": {
|
||||||
|
"_value": true,
|
||||||
|
"_option": "physics-2d-box2d"
|
||||||
|
},
|
||||||
|
"physics-2d-box2d": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"physics-2d-builtin": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"intersection-2d": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"primitive": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"profiler": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"occlusion-query": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"geometry-renderer": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"debug-renderer": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"particle-2d": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"webview": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"tween": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"websocket": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"websocket-server": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"terrain": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"light-probe": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"tiled-map": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"spine": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"dragon-bones": {
|
||||||
|
"_value": true
|
||||||
|
},
|
||||||
|
"marionette": {
|
||||||
|
"_value": false
|
||||||
|
},
|
||||||
|
"custom-pipeline": {
|
||||||
|
"_value": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"includeModules": [
|
||||||
|
"2d",
|
||||||
|
"animation",
|
||||||
|
"audio",
|
||||||
|
"base",
|
||||||
|
"dragon-bones",
|
||||||
|
"gfx-webgl",
|
||||||
|
"gfx-webgl2",
|
||||||
|
"intersection-2d",
|
||||||
|
"particle-2d",
|
||||||
|
"physics-2d-box2d",
|
||||||
|
"profiler",
|
||||||
|
"spine",
|
||||||
|
"tiled-map",
|
||||||
|
"tween",
|
||||||
|
"ui",
|
||||||
|
"video",
|
||||||
|
"webview"
|
||||||
|
],
|
||||||
|
"noDeprecatedFeatures": {
|
||||||
|
"value": false,
|
||||||
|
"version": ""
|
||||||
|
},
|
||||||
|
"flags": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
client-cocos/settings/v2/packages/information.json
Normal file
23
client-cocos/settings/v2/packages/information.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"__version__": "1.0.0",
|
||||||
|
"information": {
|
||||||
|
"customSplash": {
|
||||||
|
"id": "customSplash",
|
||||||
|
"label": "customSplash",
|
||||||
|
"enable": true,
|
||||||
|
"customSplash": {
|
||||||
|
"complete": false,
|
||||||
|
"form": "https://creator-api.cocos.com/api/form/show?sid=73288e660382f608298d6ba44c576519"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"removeSplash": {
|
||||||
|
"id": "removeSplash",
|
||||||
|
"label": "removeSplash",
|
||||||
|
"enable": true,
|
||||||
|
"removeSplash": {
|
||||||
|
"complete": false,
|
||||||
|
"form": "https://creator-api.cocos.com/api/form/show?sid=73288e660382f608298d6ba44c576519"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
client-cocos/settings/v2/packages/program.json
Normal file
3
client-cocos/settings/v2/packages/program.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__version__": "1.0.3"
|
||||||
|
}
|
||||||
3
client-cocos/settings/v2/packages/project.json
Normal file
3
client-cocos/settings/v2/packages/project.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__version__": "1.0.6"
|
||||||
|
}
|
||||||
9
client-cocos/tsconfig.json
Normal file
9
client-cocos/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
/* Base configuration. Do not edit this field. */
|
||||||
|
"extends": "./temp/tsconfig.cocos.json",
|
||||||
|
/* Add your custom configuration here. */
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": false,
|
||||||
|
"module": "CommonJS"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user