cocos基础工程

This commit is contained in:
janing
2025-11-28 18:10:10 +08:00
parent 4e4863c7e6
commit 9742bf21fa
88 changed files with 4626 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
[InternetShortcut]
URL=https://docs.cocos.com/creator/manual/en/scripting/setup.html#custom-script-template

View File

@@ -0,0 +1,5 @@
{
"image": {
"type": "sprite-frame"
}
}

24
client-cocos/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
#///////////////////////////
# Cocos Creator 3D Project
#///////////////////////////
library/
temp/
local/
build/
profiles/
native
#//////////////////////////
# NPM
#//////////////////////////
node_modules/
#//////////////////////////
# VSCode
#//////////////////////////
.vscode/
#//////////////////////////
# WebStorm
#//////////////////////////
.idea/

View 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/` - 输出目录

View 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

Binary file not shown.

View 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

Binary file not shown.

View 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

Binary file not shown.

View 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"
}
}

View 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

Binary file not shown.

View 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

Binary file not shown.

View 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

Binary file not shown.

View File

@@ -0,0 +1,11 @@
{
"ver": "1.1.49",
"importer": "scene",
"imported": true,
"uuid": "7b9f26e0-5d5a-4124-bc21-087790bc8aac",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "67d29da8-68a1-4b50-9e7c-e2883d977ac6",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "bab444c8-af0d-4a8b-ac9b-967b84f9f626",
"files": [],
"subMetas": {},
"userData": {}
}

View 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;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "257b98d4-8ead-4135-bf08-c0b7e099b1b5",
"files": [],
"subMetas": {},
"userData": {}
}

View 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);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "594d765c-8674-48a1-a60f-06e438cdfb47",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "cd3c8566-25ad-4c45-acb5-d4622f704aa6",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "c4fb263a-8354-4597-9436-e72c9e9f189d",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "03edfca7-426e-4a79-aa6f-9bd259a9ead3",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "73bc9ff5-962b-4dd1-801f-0671a621bc9b",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "d101ed2a-e68c-4b3b-831b-04d84f0596f0",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "3efea1af-73f2-404f-a948-e059b92c65c1",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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('客户端多人模式尚未实现');
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "57dd41d5-1108-443c-8888-9aa695b552ce",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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('服务器多人模式尚未实现');
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "6f237677-8531-40fc-af0a-c1e9f020528a",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "b1ed032a-5ed8-4dee-b821-8d4a4558ae00",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "e796606d-57a8-45e7-b700-66f4c28ccaed",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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'
};
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "9cdad6b2-b0b6-4223-b44f-fbdf0f37197c",
"files": [],
"subMetas": {},
"userData": {}
}

View 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';

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "618d7e5f-842b-4506-a4ad-f55fc18f102b",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "af23f3f9-fa61-4053-9a39-a4fccd83f14e",
"files": [],
"subMetas": {},
"userData": {}
}

View 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;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "1dc34aed-d9d7-4cd7-8bf7-5d4a1564882f",
"files": [],
"subMetas": {},
"userData": {}
}

View 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;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "ff4e5c1d-69eb-4234-9087-1b84b837d708",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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>;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "b15a76bd-406d-4463-8a9c-1e82229ab995",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "afa401be-4482-4d0a-ae6c-1b1b94b7b992",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "c0d94965-94bc-44ed-b580-d2b93cdb93a1",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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] 游戏重置完成');
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "f1a5b7ea-5782-43d3-9eea-8b5909a93dbd",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "2121ed3d-9094-4dde-97cb-7ba9081cbd9d",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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');
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "1a3cf9f0-ddcf-4175-9dc8-632e89c6941e",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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>;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "b07a5b72-454a-4fb1-8dd6-9b39141b85d8",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "3cc0736e-7d1c-4771-b861-bfd3525e90d1",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "0d17157f-5edf-479c-9296-4008cd31be83",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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[];
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "6a80be32-a5d9-4b97-ace6-ac2c417a91b3",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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();
}
}

View 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}`);
}
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "03de2ced-4b54-4dac-8773-b078be08a33f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "27270c02-6c7f-448a-922d-12d7734844de",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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');
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "da8038e2-7c90-4e3e-a3f6-4f8b88b3a11d",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "8d22d76a-17c1-48bd-a151-d2ed14a97029",
"files": [],
"subMetas": {},
"userData": {}
}

View 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为WASMfalse为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();

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "1a752554-ae7a-4ac3-b468-31ad25757d52",
"files": [],
"subMetas": {},
"userData": {}
}

View 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() 方法
*/

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "2ecfafcc-6d3c-443e-8e24-3207988f4742",
"files": [],
"subMetas": {},
"userData": {}
}

View 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

Binary file not shown.

View File

@@ -0,0 +1,12 @@
{
"ver": "1.0.1",
"importer": "*",
"imported": true,
"uuid": "401aec55-42ae-48c1-9160-22c930389b53",
"files": [
".json",
".wasm"
],
"subMetas": {},
"userData": {}
}

View 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

View File

@@ -0,0 +1,7 @@
{
"name": "client-cocos",
"uuid": "20f4ca2c-2427-4801-805a-4b81e119fc54",
"creator": {
"version": "3.8.1"
}
}

View File

@@ -0,0 +1,3 @@
{
"__version__": "1.3.7"
}

View File

@@ -0,0 +1,3 @@
{
"__version__": "1.0.1"
}

View 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": {}
}
}

View 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"
}
}
}
}

View File

@@ -0,0 +1,3 @@
{
"__version__": "1.0.3"
}

View File

@@ -0,0 +1,3 @@
{
"__version__": "1.0.6"
}

View 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"
}
}