cocos基础工程
This commit is contained in:
9
client-cocos/assets/scripts/Modules/Pinball/Boot.meta
Normal file
9
client-cocos/assets/scripts/Modules/Pinball/Boot.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "03edfca7-426e-4a79-aa6f-9bd259a9ead3",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 启动器基类
|
||||
* 定义不同启动模式的统一接口
|
||||
*/
|
||||
|
||||
import { Component } from 'cc';
|
||||
import { PinballManager } from '../PinballManager';
|
||||
import { PinballBootConfig } from './BootTypes';
|
||||
|
||||
export abstract class BaseBooter {
|
||||
|
||||
/**
|
||||
* 启动特定模式的游戏
|
||||
* @param hostComponent 宿主组件
|
||||
* @param config 启动配置
|
||||
*/
|
||||
abstract boot(hostComponent: Component, config: PinballBootConfig): Promise<PinballManager>;
|
||||
|
||||
/**
|
||||
* 应用配置到 PinballManager
|
||||
* @param pinballManager PinballManager 实例
|
||||
* @param config 启动配置
|
||||
*/
|
||||
protected applyConfiguration(pinballManager: PinballManager, config: PinballBootConfig): void {
|
||||
// 设置节点引用
|
||||
if (config.cameraNode) {
|
||||
pinballManager.cameraNode = config.cameraNode;
|
||||
}
|
||||
|
||||
if (config.renderContainer) {
|
||||
pinballManager.renderContainer = config.renderContainer;
|
||||
}
|
||||
|
||||
if (config.uiContainer) {
|
||||
pinballManager.uiContainer = config.uiContainer;
|
||||
}
|
||||
|
||||
// 设置基础配置
|
||||
pinballManager.autoStart = config.autoStart !== false; // 默认为 true
|
||||
pinballManager.debugMode = config.debugMode || false;
|
||||
|
||||
// 应用自定义配置
|
||||
if (config.physicsConfig || config.renderConfig || config.wasmPath) {
|
||||
const pinballConfig = pinballManager.getConfig();
|
||||
|
||||
if (!pinballConfig) {
|
||||
pinballManager.updateConfig(config);
|
||||
} else {
|
||||
if (config.physicsConfig) {
|
||||
pinballConfig.physicsSettings = {
|
||||
...pinballConfig.physicsSettings,
|
||||
gravity: config.physicsConfig.gravity || { x: 0, y: -9.81 },
|
||||
timeStep: config.physicsConfig.timeStep || 1 / 60
|
||||
};
|
||||
}
|
||||
|
||||
if (config.renderConfig) {
|
||||
pinballConfig.renderSettings = {
|
||||
...pinballConfig.renderSettings,
|
||||
enableEffects: config.renderConfig.enableEffects !== false,
|
||||
maxParticles: config.renderConfig.maxParticles || 500
|
||||
};
|
||||
}
|
||||
|
||||
if (config.wasmPath) {
|
||||
pinballConfig.wasmPath = config.wasmPath;
|
||||
}
|
||||
|
||||
pinballManager.updateConfig(pinballConfig);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件日志输出
|
||||
*/
|
||||
protected log(message: string, enabled?: boolean): void {
|
||||
if (enabled !== false) {
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "73bc9ff5-962b-4dd1-801f-0671a621bc9b",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Pinball 启动配置接口
|
||||
*/
|
||||
|
||||
import { Node } from 'cc';
|
||||
|
||||
export enum PinballBootMode {
|
||||
STANDALONE = 'standalone',
|
||||
CLIENT_MULTIPLAYER = 'client-multiplayer',
|
||||
SERVER_MULTIPLAYER = 'server-multiplayer'
|
||||
}
|
||||
|
||||
export interface PinballBootConfig {
|
||||
/** 启动模式 */
|
||||
mode: PinballBootMode;
|
||||
|
||||
/** 主相机节点 */
|
||||
cameraNode?: Node;
|
||||
|
||||
/** 渲染容器节点 */
|
||||
renderContainer?: Node;
|
||||
|
||||
/** UI容器节点 */
|
||||
uiContainer?: Node;
|
||||
|
||||
/** 是否启用调试模式 */
|
||||
debugMode?: boolean;
|
||||
|
||||
/** 是否自动启动 */
|
||||
autoStart?: boolean;
|
||||
|
||||
/** 物理引擎配置 */
|
||||
physicsConfig?: {
|
||||
gravity?: { x: number; y: number };
|
||||
timeStep?: number;
|
||||
};
|
||||
|
||||
/** 渲染配置 */
|
||||
renderConfig?: {
|
||||
enableEffects?: boolean;
|
||||
maxParticles?: number;
|
||||
};
|
||||
|
||||
/** 多人模式配置 */
|
||||
multiplayerConfig?: {
|
||||
serverAddress?: string;
|
||||
playerName?: string;
|
||||
roomId?: string;
|
||||
};
|
||||
|
||||
/** WASM文件路径 */
|
||||
wasmPath?: string;
|
||||
}
|
||||
|
||||
export interface PinballBootResult {
|
||||
/** 启动是否成功 */
|
||||
success: boolean;
|
||||
|
||||
/** 错误信息(如果失败) */
|
||||
error?: string;
|
||||
|
||||
/** 启动的模式 */
|
||||
mode: PinballBootMode;
|
||||
|
||||
/** PinballManager 实例 */
|
||||
pinballManager?: any;
|
||||
|
||||
/** 启动时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "d101ed2a-e68c-4b3b-831b-04d84f0596f0",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "3efea1af-73f2-404f-a948-e059b92c65c1",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Client Multiplayer 模式启动器
|
||||
*/
|
||||
|
||||
import { Component } from 'cc';
|
||||
import { PinballManager } from '../../PinballManager';
|
||||
import { BaseBooter } from '../BaseBooter';
|
||||
import { PinballBootConfig } from '../BootTypes';
|
||||
|
||||
export class ClientMultiplayerBooter extends BaseBooter {
|
||||
|
||||
async boot(hostComponent: Component, config: PinballBootConfig): Promise<PinballManager> {
|
||||
this.log('[ClientMultiplayerBooter] 正在启动客户端多人模式...', config.debugMode);
|
||||
|
||||
// TODO: 实现客户端多人模式启动逻辑
|
||||
// 1. 连接到游戏服务器
|
||||
// 2. 加入游戏房间
|
||||
// 3. 设置网络同步和状态管理
|
||||
|
||||
throw new Error('客户端多人模式尚未实现');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "57dd41d5-1108-443c-8888-9aa695b552ce",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Server Multiplayer 模式启动器
|
||||
*/
|
||||
|
||||
import { Component } from 'cc';
|
||||
import { PinballManager } from '../../PinballManager';
|
||||
import { BaseBooter } from '../BaseBooter';
|
||||
import { PinballBootConfig } from '../BootTypes';
|
||||
|
||||
export class ServerMultiplayerBooter extends BaseBooter {
|
||||
|
||||
async boot(hostComponent: Component, config: PinballBootConfig): Promise<PinballManager> {
|
||||
this.log('[ServerMultiplayerBooter] 正在启动服务器多人模式...', config.debugMode);
|
||||
|
||||
// TODO: 实现服务器多人模式启动逻辑
|
||||
// 1. 连接到 SpacetimeDB
|
||||
// 2. 创建房间或加入现有房间
|
||||
// 3. 设置网络同步
|
||||
|
||||
throw new Error('服务器多人模式尚未实现');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "6f237677-8531-40fc-af0a-c1e9f020528a",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Standalone 模式启动器
|
||||
*/
|
||||
|
||||
import { Component } from 'cc';
|
||||
import { PinballManager, PinballMode } from '../../PinballManager';
|
||||
import { BaseBooter } from '../BaseBooter';
|
||||
import { PinballBootConfig } from '../BootTypes';
|
||||
|
||||
export class StandaloneBooter extends BaseBooter {
|
||||
|
||||
async boot(hostComponent: Component, config: PinballBootConfig): Promise<PinballManager> {
|
||||
this.log('[StandaloneBooter] 正在启动 Standalone 模式...', config.debugMode);
|
||||
|
||||
// 创建或获取 PinballManager 组件
|
||||
let pinballManager = hostComponent.getComponent(PinballManager);
|
||||
if (!pinballManager) {
|
||||
pinballManager = hostComponent.addComponent(PinballManager);
|
||||
}
|
||||
|
||||
// 应用配置
|
||||
this.applyConfiguration(pinballManager, config);
|
||||
|
||||
// 设置默认模式
|
||||
pinballManager.defaultMode = PinballMode.STANDALONE;
|
||||
|
||||
// 启动 PinballManager(在配置应用后)
|
||||
const startSuccess = await pinballManager.Start();
|
||||
if (!startSuccess) {
|
||||
throw new Error('PinballManager 启动失败');
|
||||
}
|
||||
|
||||
// 启动 Standalone 模式
|
||||
const success = await pinballManager.startGame(PinballMode.STANDALONE);
|
||||
if (!success) {
|
||||
throw new Error('Standalone 模式启动失败');
|
||||
}
|
||||
|
||||
this.log('[StandaloneBooter] Standalone 模式启动完成', config.debugMode);
|
||||
return pinballManager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b1ed032a-5ed8-4dee-b821-8d4a4558ae00",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Pinball 启动工具类
|
||||
* 提供常用的启动配置和便捷方法
|
||||
*/
|
||||
|
||||
import { Component } from 'cc';
|
||||
import { PinballBootConfig, PinballBootMode, PinballBootResult, PinballBootstrap } from './index';
|
||||
|
||||
export class PinballBootUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 创建高性能配置(适合移动设备)
|
||||
*/
|
||||
public static createMobileConfig(mode: PinballBootMode): PinballBootConfig {
|
||||
const config = PinballBootstrap.createDefaultConfig(mode);
|
||||
|
||||
// 移动设备优化配置
|
||||
config.renderConfig = {
|
||||
enableEffects: false, // 关闭粒子效果以提升性能
|
||||
maxParticles: 100 // 降低粒子数量
|
||||
};
|
||||
|
||||
config.physicsConfig = {
|
||||
gravity: { x: 0, y: -9.81 },
|
||||
timeStep: 1 / 30 // 降低物理步进频率
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建高质量配置(适合桌面设备)
|
||||
*/
|
||||
public static createDesktopConfig(mode: PinballBootMode): PinballBootConfig {
|
||||
const config = PinballBootstrap.createDefaultConfig(mode);
|
||||
|
||||
// 桌面设备高质量配置
|
||||
config.renderConfig = {
|
||||
enableEffects: true, // 启用所有粒子效果
|
||||
maxParticles: 1000 // 更多粒子数量
|
||||
};
|
||||
|
||||
config.physicsConfig = {
|
||||
gravity: { x: 0, y: -9.81 },
|
||||
timeStep: 1 / 120 // 更高的物理步进频率
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建调试配置
|
||||
*/
|
||||
public static createDebugConfig(mode: PinballBootMode): PinballBootConfig {
|
||||
const config = PinballBootstrap.createDefaultConfig(mode);
|
||||
config.debugMode = true;
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建生产配置
|
||||
*/
|
||||
public static createProductionConfig(mode: PinballBootMode): PinballBootConfig {
|
||||
const config = PinballBootstrap.createDefaultConfig(mode);
|
||||
config.debugMode = false;
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量启动多个模式(用于测试)
|
||||
*/
|
||||
public static async batchBoot(
|
||||
hostComponent: Component,
|
||||
configs: PinballBootConfig[]
|
||||
): Promise<PinballBootResult[]> {
|
||||
const bootstrap = PinballBootstrap.getInstance();
|
||||
const results: PinballBootResult[] = [];
|
||||
|
||||
for (const config of configs) {
|
||||
console.log(`[PinballBootUtils] 批量启动: ${config.mode}`);
|
||||
const result = await bootstrap.boot(hostComponent, config);
|
||||
results.push(result);
|
||||
|
||||
// 如果启动失败,停止批量启动
|
||||
if (!result.success) {
|
||||
console.error(`[PinballBootUtils] 批量启动在 ${config.mode} 模式失败: ${result.error}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能基准测试启动
|
||||
*/
|
||||
public static async benchmarkBoot(
|
||||
hostComponent: Component,
|
||||
config: PinballBootConfig,
|
||||
iterations: number = 5
|
||||
): Promise<{ averageTime: number; results: PinballBootResult[] }> {
|
||||
const bootstrap = PinballBootstrap.getInstance();
|
||||
const results: PinballBootResult[] = [];
|
||||
let totalTime = 0;
|
||||
|
||||
console.log(`[PinballBootUtils] 开始性能基准测试,迭代次数: ${iterations}`);
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const startTime = performance.now();
|
||||
const result = await bootstrap.boot(hostComponent, config);
|
||||
const endTime = performance.now();
|
||||
|
||||
const duration = endTime - startTime;
|
||||
totalTime += duration;
|
||||
|
||||
results.push(result);
|
||||
|
||||
console.log(`[PinballBootUtils] 第 ${i + 1} 次启动耗时: ${duration.toFixed(2)}ms`);
|
||||
|
||||
// 清理资源准备下一次测试
|
||||
if (result.success && result.pinballManager) {
|
||||
await result.pinballManager.stopCurrentGame();
|
||||
}
|
||||
}
|
||||
|
||||
const averageTime = totalTime / iterations;
|
||||
console.log(`[PinballBootUtils] 平均启动时间: ${averageTime.toFixed(2)}ms`);
|
||||
|
||||
return { averageTime, results };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证启动结果
|
||||
*/
|
||||
public static validateBootResult(result: PinballBootResult): {
|
||||
isValid: boolean;
|
||||
issues: string[];
|
||||
} {
|
||||
const issues: string[] = [];
|
||||
|
||||
if (!result.success) {
|
||||
issues.push(`启动失败: ${result.error}`);
|
||||
}
|
||||
|
||||
if (!result.pinballManager) {
|
||||
issues.push('PinballManager 实例不存在');
|
||||
}
|
||||
|
||||
if (!result.mode) {
|
||||
issues.push('启动模式未定义');
|
||||
}
|
||||
|
||||
if (!result.timestamp || result.timestamp <= 0) {
|
||||
issues.push('时间戳无效');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: issues.length === 0,
|
||||
issues
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成启动报告
|
||||
*/
|
||||
public static generateBootReport(results: PinballBootResult[]): string {
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failureCount = results.length - successCount;
|
||||
|
||||
let report = '=== Pinball 启动报告 ===\n';
|
||||
report += `总启动次数: ${results.length}\n`;
|
||||
report += `成功启动: ${successCount}\n`;
|
||||
report += `启动失败: ${failureCount}\n`;
|
||||
report += `成功率: ${((successCount / results.length) * 100).toFixed(1)}%\n\n`;
|
||||
|
||||
if (failureCount > 0) {
|
||||
report += '失败详情:\n';
|
||||
results.filter(r => !r.success).forEach((result, index) => {
|
||||
report += `${index + 1}. ${result.mode}: ${result.error}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e796606d-57a8-45e7-b700-66f4c28ccaed",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Pinball 启动管理器
|
||||
* 负责维护游戏启动状态和调度具体启动器
|
||||
*/
|
||||
|
||||
import { Component } from 'cc';
|
||||
import { PinballBootConfig, PinballBootMode, PinballBootResult } from './BootTypes';
|
||||
import { ClientMultiplayerBooter } from './Mode/ClientMultiplayerBooter';
|
||||
import { ServerMultiplayerBooter } from './Mode/ServerMultiplayerBooter';
|
||||
import { StandaloneBooter } from './Mode/StandaloneBooter';
|
||||
|
||||
export class PinballBootstrap {
|
||||
private static instance: PinballBootstrap;
|
||||
private currentResult: PinballBootResult | null = null;
|
||||
|
||||
/** 获取单例实例 */
|
||||
public static getInstance(): PinballBootstrap {
|
||||
if (!PinballBootstrap.instance) {
|
||||
PinballBootstrap.instance = new PinballBootstrap();
|
||||
}
|
||||
return PinballBootstrap.instance;
|
||||
}
|
||||
|
||||
private constructor() { }
|
||||
|
||||
/**
|
||||
* 启动 Pinball 游戏
|
||||
*/
|
||||
public async boot(hostComponent: Component, config: PinballBootConfig): Promise<PinballBootResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
console.log(`[PinballBootstrap] 开始启动 ${config.mode} 模式`);
|
||||
|
||||
// 获取对应的启动器
|
||||
const booter = this.getBooter(config.mode);
|
||||
|
||||
// 执行启动
|
||||
const pinballManager = await booter.boot(hostComponent, config);
|
||||
|
||||
// 记录启动结果
|
||||
this.currentResult = {
|
||||
success: true,
|
||||
mode: config.mode,
|
||||
pinballManager,
|
||||
timestamp: startTime
|
||||
};
|
||||
|
||||
console.log(`[PinballBootstrap] ${config.mode} 模式启动成功`);
|
||||
return this.currentResult;
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[PinballBootstrap] 启动失败:`, error);
|
||||
|
||||
this.currentResult = {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
mode: config.mode,
|
||||
timestamp: startTime
|
||||
};
|
||||
|
||||
return this.currentResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启动器实例
|
||||
*/
|
||||
private getBooter(mode: PinballBootMode) {
|
||||
switch (mode) {
|
||||
case PinballBootMode.STANDALONE:
|
||||
return new StandaloneBooter();
|
||||
case PinballBootMode.CLIENT_MULTIPLAYER:
|
||||
return new ClientMultiplayerBooter();
|
||||
case PinballBootMode.SERVER_MULTIPLAYER:
|
||||
return new ServerMultiplayerBooter();
|
||||
default:
|
||||
throw new Error(`不支持的启动模式: ${mode}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前启动结果
|
||||
*/
|
||||
public getCurrentResult(): PinballBootResult | null {
|
||||
return this.currentResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除启动状态
|
||||
*/
|
||||
public clearState(): void {
|
||||
this.currentResult = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认配置
|
||||
*/
|
||||
public static createDefaultConfig(mode: PinballBootMode): PinballBootConfig {
|
||||
return {
|
||||
mode,
|
||||
debugMode: true,
|
||||
autoStart: true,
|
||||
physicsConfig: {
|
||||
gravity: { x: 0, y: -9.81 },
|
||||
timeStep: 1 / 60
|
||||
},
|
||||
renderConfig: {
|
||||
enableEffects: true,
|
||||
maxParticles: 500
|
||||
},
|
||||
wasmPath: 'assets/wasm/pinball_physics.wasm'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "9cdad6b2-b0b6-4223-b44f-fbdf0f37197c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
12
client-cocos/assets/scripts/Modules/Pinball/Boot/index.ts
Normal file
12
client-cocos/assets/scripts/Modules/Pinball/Boot/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Pinball Boot 模块入口
|
||||
* 导出启动相关的所有类型和类
|
||||
*/
|
||||
|
||||
export { PinballBootMode } from './BootTypes';
|
||||
export type {
|
||||
PinballBootConfig,
|
||||
PinballBootResult
|
||||
} from './BootTypes';
|
||||
export { PinballBootstrap } from './PinballBootstrap';
|
||||
export { PinballBootUtils } from './PinballBootUtils';
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "618d7e5f-842b-4506-a4ad-f55fc18f102b",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client-cocos/assets/scripts/Modules/Pinball/Core.meta
Normal file
9
client-cocos/assets/scripts/Modules/Pinball/Core.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "af23f3f9-fa61-4053-9a39-a4fccd83f14e",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
88
client-cocos/assets/scripts/Modules/Pinball/Core/EventBus.ts
Normal file
88
client-cocos/assets/scripts/Modules/Pinball/Core/EventBus.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 简单的事件总线实现
|
||||
* 用于 Pinball 模块内部组件间通信
|
||||
*/
|
||||
|
||||
export type EventCallback<T = any> = (data: T) => void;
|
||||
|
||||
export class EventBus {
|
||||
private static instance: EventBus;
|
||||
private events: Map<string, EventCallback[]> = new Map();
|
||||
|
||||
private constructor() { }
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
static getInstance(): EventBus {
|
||||
if (!EventBus.instance) {
|
||||
EventBus.instance = new EventBus();
|
||||
}
|
||||
return EventBus.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅事件
|
||||
*/
|
||||
on<T = any>(event: string, callback: EventCallback<T>): void {
|
||||
if (!this.events.has(event)) {
|
||||
this.events.set(event, []);
|
||||
}
|
||||
this.events.get(event)!.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅事件
|
||||
*/
|
||||
off<T = any>(event: string, callback: EventCallback<T>): void {
|
||||
const callbacks = this.events.get(event);
|
||||
if (callbacks) {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发射事件
|
||||
*/
|
||||
emit<T = any>(event: string, data?: T): void {
|
||||
const callbacks = this.events.get(event);
|
||||
if (callbacks) {
|
||||
callbacks.forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`Error in event callback for event '${event}':`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 只订阅一次事件
|
||||
*/
|
||||
once<T = any>(event: string, callback: EventCallback<T>): void {
|
||||
const onceCallback: EventCallback<T> = (data: T) => {
|
||||
callback(data);
|
||||
this.off(event, onceCallback);
|
||||
};
|
||||
this.on(event, onceCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有事件监听器
|
||||
*/
|
||||
clear(): void {
|
||||
this.events.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件监听器数量
|
||||
*/
|
||||
listenerCount(event: string): number {
|
||||
const callbacks = this.events.get(event);
|
||||
return callbacks ? callbacks.length : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "1dc34aed-d9d7-4cd7-8bf7-5d4a1564882f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
74
client-cocos/assets/scripts/Modules/Pinball/Core/GameData.ts
Normal file
74
client-cocos/assets/scripts/Modules/Pinball/Core/GameData.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Pinball 游戏核心数据结构定义
|
||||
*/
|
||||
|
||||
/** 2D 向量 */
|
||||
export interface Vector2 {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/** 物理体 ID */
|
||||
export type BodyId = number;
|
||||
|
||||
/** 世界 ID */
|
||||
export type WorldId = number;
|
||||
|
||||
/** 物理体数据 */
|
||||
export interface PhysicsBodyData {
|
||||
id: BodyId;
|
||||
position: Vector2;
|
||||
velocity: Vector2;
|
||||
rotation: number;
|
||||
angularVelocity: number;
|
||||
bodyType: 'circle' | 'box';
|
||||
radius?: number;
|
||||
size?: Vector2;
|
||||
isStatic: boolean;
|
||||
}
|
||||
|
||||
/** 游戏状态 */
|
||||
export interface GameState {
|
||||
worldId: WorldId;
|
||||
bodies: Map<BodyId, PhysicsBodyData>;
|
||||
isPaused: boolean;
|
||||
timeStep: number;
|
||||
}
|
||||
|
||||
/** Pinball 配置 */
|
||||
export interface PinballConfig {
|
||||
mode: 'standalone' | 'client-multiplayer' | 'server-multiplayer';
|
||||
serverAddress?: string;
|
||||
wasmPath?: string;
|
||||
physicsSettings?: {
|
||||
gravity: Vector2;
|
||||
timeStep: number;
|
||||
};
|
||||
renderSettings?: {
|
||||
enableEffects: boolean;
|
||||
maxParticles: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** 物理设置 */
|
||||
export interface PhysicsSettings {
|
||||
gravity: Vector2;
|
||||
timeStep: number;
|
||||
maxBodies: number;
|
||||
}
|
||||
|
||||
/** 游戏事件类型 */
|
||||
export enum GameEventType {
|
||||
PHYSICS_STEP = 'physics_step',
|
||||
BODY_CREATED = 'body_created',
|
||||
BODY_DESTROYED = 'body_destroyed',
|
||||
WORLD_RESET = 'world_reset',
|
||||
INPUT_RECEIVED = 'input_received'
|
||||
}
|
||||
|
||||
/** 游戏事件数据 */
|
||||
export interface GameEvent {
|
||||
type: GameEventType;
|
||||
data?: any;
|
||||
timestamp: number;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ff4e5c1d-69eb-4234-9087-1b84b837d708",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 物理引擎接口
|
||||
* 定义了不同物理引擎实现需要遵循的契约
|
||||
*/
|
||||
|
||||
import { BodyId, PhysicsBodyData, PhysicsSettings, Vector2, WorldId } from './GameData';
|
||||
|
||||
export interface CreateCircleOptions {
|
||||
position: Vector2;
|
||||
radius: number;
|
||||
isStatic: boolean;
|
||||
density?: number;
|
||||
restitution?: number;
|
||||
friction?: number;
|
||||
}
|
||||
|
||||
export interface CreateBoxOptions {
|
||||
position: Vector2;
|
||||
size: Vector2;
|
||||
isStatic: boolean;
|
||||
density?: number;
|
||||
restitution?: number;
|
||||
friction?: number;
|
||||
}
|
||||
|
||||
export interface IPhysicsEngine {
|
||||
/**
|
||||
* 初始化物理引擎
|
||||
*/
|
||||
initialize(settings?: PhysicsSettings): Promise<void>;
|
||||
|
||||
/**
|
||||
* 创建物理世界
|
||||
*/
|
||||
createWorld(gravity: Vector2): Promise<WorldId>;
|
||||
|
||||
/**
|
||||
* 销毁物理世界
|
||||
*/
|
||||
destroyWorld(worldId: WorldId): Promise<void>;
|
||||
|
||||
/**
|
||||
* 执行物理步进
|
||||
*/
|
||||
step(deltaTime: number, worldId?: WorldId): Promise<void>;
|
||||
|
||||
/**
|
||||
* 创建圆形刚体
|
||||
*/
|
||||
createCircle(options: CreateCircleOptions): BodyId;
|
||||
|
||||
/**
|
||||
* 创建矩形刚体
|
||||
*/
|
||||
createBox(options: CreateBoxOptions): BodyId;
|
||||
|
||||
/**
|
||||
* 创建动态刚体
|
||||
*/
|
||||
createDynamicBody(worldId: WorldId, position: Vector2, radius: number): Promise<BodyId>;
|
||||
|
||||
/**
|
||||
* 创建静态刚体
|
||||
*/
|
||||
createStaticBody(worldId: WorldId, position: Vector2, radius: number): Promise<BodyId>;
|
||||
|
||||
/**
|
||||
* 销毁刚体
|
||||
*/
|
||||
destroyBody(worldId: WorldId, bodyId: BodyId): Promise<void>;
|
||||
|
||||
/**
|
||||
* 移除刚体
|
||||
*/
|
||||
removeBody(bodyId: BodyId): void;
|
||||
|
||||
/**
|
||||
* 获取刚体位置
|
||||
*/
|
||||
getBodyPosition(worldId: WorldId, bodyId: BodyId): Promise<Vector2 | null>;
|
||||
|
||||
/**
|
||||
* 设置刚体位置
|
||||
*/
|
||||
setBodyPosition(worldId: WorldId, bodyId: BodyId, position: Vector2): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取刚体速度
|
||||
*/
|
||||
getBodyVelocity(worldId: WorldId, bodyId: BodyId): Promise<Vector2 | null>;
|
||||
|
||||
/**
|
||||
* 设置刚体速度
|
||||
*/
|
||||
setBodyVelocity(worldId: WorldId, bodyId: BodyId, velocity: Vector2): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取刚体数据
|
||||
*/
|
||||
getBodyData(bodyId: BodyId): PhysicsBodyData | null;
|
||||
|
||||
/**
|
||||
* 获取所有物理体数据
|
||||
*/
|
||||
getAllBodies(worldId: WorldId): Promise<PhysicsBodyData[]>;
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
cleanup(): void;
|
||||
|
||||
/**
|
||||
* 清理资源 (别名)
|
||||
*/
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b15a76bd-406d-4463-8a9c-1e82229ab995",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 渲染器接口
|
||||
* 定义了渲染系统需要实现的方法
|
||||
*/
|
||||
|
||||
import { PhysicsBodyData, Vector2 } from './GameData';
|
||||
|
||||
/** 渲染对象数据 */
|
||||
export interface RenderObject {
|
||||
id: string;
|
||||
position: Vector2;
|
||||
radius: number;
|
||||
color: { r: number; g: number; b: number; a: number };
|
||||
layer: number;
|
||||
}
|
||||
|
||||
/** 粒子效果数据 */
|
||||
export interface ParticleEffect {
|
||||
position: Vector2;
|
||||
velocity: Vector2;
|
||||
color: { r: number; g: number; b: number; a: number };
|
||||
lifetime: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface IRenderer {
|
||||
/**
|
||||
* 初始化渲染器
|
||||
*/
|
||||
initialize(parentNode: any): Promise<void>;
|
||||
|
||||
/**
|
||||
* 渲染物理体
|
||||
*/
|
||||
renderBodies(bodies: PhysicsBodyData[]): void;
|
||||
|
||||
/**
|
||||
* 创建渲染对象
|
||||
*/
|
||||
createRenderObject(body: PhysicsBodyData): RenderObject;
|
||||
|
||||
/**
|
||||
* 更新渲染对象
|
||||
*/
|
||||
updateRenderObject(renderObject: RenderObject, body: PhysicsBodyData): void;
|
||||
|
||||
/**
|
||||
* 移除渲染对象
|
||||
*/
|
||||
removeRenderObject(bodyId: string): void;
|
||||
|
||||
/**
|
||||
* 播放粒子效果
|
||||
*/
|
||||
playParticleEffect(effect: ParticleEffect): void;
|
||||
|
||||
/**
|
||||
* 清除所有渲染对象
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* 设置相机
|
||||
*/
|
||||
setCamera(camera: any): void;
|
||||
|
||||
/**
|
||||
* 设置世界边界
|
||||
*/
|
||||
setWorldBounds(width: number, height: number): void;
|
||||
|
||||
/**
|
||||
* 设置相机位置
|
||||
*/
|
||||
setCameraPosition(position: Vector2): void;
|
||||
|
||||
/**
|
||||
* 设置相机缩放
|
||||
*/
|
||||
setCameraZoom(zoom: number): void;
|
||||
|
||||
/**
|
||||
* 销毁渲染器
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "afa401be-4482-4d0a-ae6c-1b1b94b7b992",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "c0d94965-94bc-44ed-b580-d2b93cdb93a1",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import { _decorator, Camera, Component, Node } from 'cc';
|
||||
import { EventBus } from '../Core/EventBus';
|
||||
import { PhysicsBodyData, Vector2 } from '../Core/GameData';
|
||||
import { IPhysicsEngine } from '../Core/IPhysicsEngine';
|
||||
import { IRenderer } from '../Core/IRenderer';
|
||||
import { InputManager } from '../Input/InputManager';
|
||||
import { MouseInputEvent, TouchInputEvent } from '../Input/InputTypes';
|
||||
import { WasmPhysicsEngine } from '../Physics/WasmPhysicsEngine';
|
||||
import { PinballRenderer } from '../Renderer/PinballRenderer';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* Standalone模式 - 单机弹珠物理游戏模式
|
||||
* 整合物理引擎、渲染器和输入管理器,提供完整的单机游戏体验
|
||||
*/
|
||||
@ccclass('StandaloneMode')
|
||||
export class StandaloneMode extends Component {
|
||||
|
||||
@property(Camera)
|
||||
gameCamera: Camera = null;
|
||||
|
||||
@property(Node)
|
||||
renderNode: Node = null;
|
||||
|
||||
@property({ type: Node, tooltip: "用于显示游戏边界的节点" })
|
||||
boundsNode: Node = null;
|
||||
|
||||
// 核心系统
|
||||
private physicsEngine: IPhysicsEngine = null;
|
||||
private renderer: IRenderer = null;
|
||||
private inputManager: InputManager = null;
|
||||
private eventBus: EventBus = null;
|
||||
|
||||
// 游戏配置
|
||||
@property({ tooltip: "游戏世界宽度" })
|
||||
worldWidth: number = 800;
|
||||
|
||||
@property({ tooltip: "游戏世界高度" })
|
||||
worldHeight: number = 600;
|
||||
|
||||
@property({ tooltip: "重力加速度" })
|
||||
gravity: number = -9.81;
|
||||
|
||||
@property({ tooltip: "弹珠默认半径" })
|
||||
ballRadius: number = 10;
|
||||
|
||||
@property({ tooltip: "弹珠默认密度" })
|
||||
ballDensity: number = 1.0;
|
||||
|
||||
@property({ tooltip: "弹珠默认弹性系数" })
|
||||
ballRestitution: number = 0.8;
|
||||
|
||||
// 游戏状态
|
||||
private isInitialized: boolean = false;
|
||||
private ballCount: number = 0;
|
||||
private activeBalls: Map<number, PhysicsBodyData> = new Map();
|
||||
|
||||
async onLoad() {
|
||||
// 初始化事件总线
|
||||
this.eventBus = EventBus.getInstance();
|
||||
|
||||
// 初始化物理引擎
|
||||
await this.initializePhysics();
|
||||
|
||||
// 初始化渲染器
|
||||
this.initializeRenderer();
|
||||
|
||||
// 初始化输入管理器
|
||||
this.initializeInput();
|
||||
|
||||
// 注册事件监听
|
||||
this.registerEventHandlers();
|
||||
|
||||
console.log('[StandaloneMode] 初始化完成');
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
if (this.isInitialized) {
|
||||
this.startGameLoop();
|
||||
}
|
||||
}
|
||||
|
||||
onDisable() {
|
||||
this.stopGameLoop();
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化物理引擎
|
||||
*/
|
||||
private async initializePhysics(): Promise<void> {
|
||||
this.physicsEngine = new WasmPhysicsEngine();
|
||||
await this.physicsEngine.initialize({
|
||||
gravity: { x: 0, y: this.gravity },
|
||||
timeStep: 1 / 60,
|
||||
maxBodies: 1000
|
||||
});
|
||||
|
||||
// 创建物理世界
|
||||
await this.physicsEngine.createWorld({ x: 0, y: this.gravity });
|
||||
|
||||
// 创建世界边界
|
||||
this.createWorldBounds();
|
||||
|
||||
console.log('[StandaloneMode] 物理引擎初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化渲染器
|
||||
*/
|
||||
private initializeRenderer(): void {
|
||||
if (!this.renderNode) {
|
||||
console.error('[StandaloneMode] renderNode 未设置');
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderer = this.renderNode.getComponent(PinballRenderer);
|
||||
if (!this.renderer) {
|
||||
this.renderer = this.renderNode.addComponent(PinballRenderer);
|
||||
}
|
||||
|
||||
// 设置渲染器参数
|
||||
if (this.gameCamera) {
|
||||
this.renderer.setCamera(this.gameCamera);
|
||||
}
|
||||
|
||||
this.renderer.setWorldBounds(this.worldWidth, this.worldHeight);
|
||||
|
||||
console.log('[StandaloneMode] 渲染器初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化输入管理器
|
||||
*/
|
||||
private initializeInput(): void {
|
||||
this.inputManager = this.getComponent(InputManager);
|
||||
if (!this.inputManager) {
|
||||
this.inputManager = this.addComponent(InputManager);
|
||||
}
|
||||
|
||||
// 设置输入管理器参数
|
||||
if (this.gameCamera) {
|
||||
this.inputManager.setCamera(this.gameCamera);
|
||||
}
|
||||
|
||||
console.log('[StandaloneMode] 输入管理器初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册事件处理器
|
||||
*/
|
||||
private registerEventHandlers(): void {
|
||||
// 监听输入事件
|
||||
this.eventBus.on('input.mouse.click', (event: MouseInputEvent) => this.onMouseClick(event));
|
||||
this.eventBus.on('input.touch.start', (event: TouchInputEvent) => this.onTouchStart(event));
|
||||
|
||||
// 监听物理事件
|
||||
this.eventBus.on('physics.collision', (collisionData: any) => this.onPhysicsCollision(collisionData));
|
||||
|
||||
console.log('[StandaloneMode] 事件处理器注册完成');
|
||||
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建世界边界
|
||||
*/
|
||||
private createWorldBounds(): void {
|
||||
const halfWidth = this.worldWidth / 2;
|
||||
const halfHeight = this.worldHeight / 2;
|
||||
const wallThickness = 10;
|
||||
|
||||
// 创建四面墙壁
|
||||
const walls = [
|
||||
// 底部墙
|
||||
{ x: 0, y: -halfHeight - wallThickness / 2, width: this.worldWidth + wallThickness, height: wallThickness },
|
||||
// 顶部墙
|
||||
{ x: 0, y: halfHeight + wallThickness / 2, width: this.worldWidth + wallThickness, height: wallThickness },
|
||||
// 左侧墙
|
||||
{ x: -halfWidth - wallThickness / 2, y: 0, width: wallThickness, height: this.worldHeight + wallThickness },
|
||||
// 右侧墙
|
||||
{ x: halfWidth + wallThickness / 2, y: 0, width: wallThickness, height: this.worldHeight + wallThickness }
|
||||
];
|
||||
|
||||
for (const wall of walls) {
|
||||
this.physicsEngine.createBox({
|
||||
position: { x: wall.x, y: wall.y },
|
||||
size: { x: wall.width, y: wall.height },
|
||||
isStatic: true,
|
||||
restitution: 0.8,
|
||||
friction: 0.3
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[StandaloneMode] 世界边界创建完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标点击事件处理
|
||||
*/
|
||||
private onMouseClick(event: MouseInputEvent): void {
|
||||
this.createBallAtPosition(event.position);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸开始事件处理
|
||||
*/
|
||||
private onTouchStart(event: TouchInputEvent): void {
|
||||
this.createBallAtPosition(event.position);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定位置创建弹珠
|
||||
*/
|
||||
private createBallAtPosition(position: Vector2): void {
|
||||
const ballId = this.physicsEngine.createCircle({
|
||||
position: position,
|
||||
radius: this.ballRadius,
|
||||
isStatic: false,
|
||||
density: this.ballDensity,
|
||||
restitution: this.ballRestitution,
|
||||
friction: 0.3
|
||||
});
|
||||
|
||||
// 创建球体数据
|
||||
const ballData: PhysicsBodyData = {
|
||||
id: ballId,
|
||||
position: position,
|
||||
rotation: 0,
|
||||
velocity: { x: 0, y: 0 },
|
||||
angularVelocity: 0,
|
||||
bodyType: 'circle',
|
||||
radius: this.ballRadius,
|
||||
isStatic: false
|
||||
};
|
||||
|
||||
this.activeBalls.set(ballId, ballData);
|
||||
this.ballCount++;
|
||||
|
||||
// 通知渲染器
|
||||
this.eventBus.emit('ball.created', {
|
||||
id: ballId,
|
||||
position: position,
|
||||
radius: this.ballRadius
|
||||
});
|
||||
|
||||
console.log(`[StandaloneMode] 创建弹珠 #${ballId} 在位置 (${position.x}, ${position.y})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理碰撞事件处理
|
||||
*/
|
||||
private onPhysicsCollision(collisionData: any): void {
|
||||
// 播放碰撞音效或效果
|
||||
console.log('[StandaloneMode] 物理碰撞:', collisionData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始游戏循环
|
||||
*/
|
||||
private startGameLoop(): void {
|
||||
// 在update中已经自动运行物理和渲染循环
|
||||
console.log('[StandaloneMode] 游戏循环已开始');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止游戏循环
|
||||
*/
|
||||
private stopGameLoop(): void {
|
||||
console.log('[StandaloneMode] 游戏循环已停止');
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏更新循环
|
||||
*/
|
||||
update(deltaTime: number) {
|
||||
if (!this.isInitialized || !this.physicsEngine) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 步进物理引擎
|
||||
this.physicsEngine.step(deltaTime);
|
||||
|
||||
// 更新所有活动球体的状态
|
||||
for (const [ballId, ballData] of this.activeBalls) {
|
||||
const updatedData = this.physicsEngine.getBodyData(ballId);
|
||||
if (updatedData) {
|
||||
// 更新本地数据
|
||||
ballData.position = updatedData.position;
|
||||
ballData.rotation = updatedData.rotation;
|
||||
ballData.velocity = updatedData.velocity;
|
||||
ballData.angularVelocity = updatedData.angularVelocity;
|
||||
|
||||
// 发送更新事件给渲染器
|
||||
this.eventBus.emit('ball.updated', {
|
||||
id: ballId,
|
||||
position: ballData.position,
|
||||
rotation: ballData.rotation
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
private cleanup(): void {
|
||||
// 清理事件监听器
|
||||
this.eventBus.off('input.mouse.click', this.onMouseClick);
|
||||
this.eventBus.off('input.touch.start', this.onTouchStart);
|
||||
this.eventBus.off('physics.collision', this.onPhysicsCollision);
|
||||
|
||||
// 清理物理引擎
|
||||
if (this.physicsEngine) {
|
||||
this.physicsEngine.cleanup();
|
||||
this.physicsEngine = null;
|
||||
}
|
||||
|
||||
// 清理数据
|
||||
this.activeBalls.clear();
|
||||
this.ballCount = 0;
|
||||
this.isInitialized = false;
|
||||
|
||||
console.log('[StandaloneMode] 资源清理完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取游戏统计信息
|
||||
*/
|
||||
public getGameStats() {
|
||||
return {
|
||||
ballCount: this.ballCount,
|
||||
activeBalls: this.activeBalls.size,
|
||||
worldSize: { width: this.worldWidth, height: this.worldHeight },
|
||||
isInitialized: this.isInitialized
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置游戏
|
||||
*/
|
||||
public resetGame(): void {
|
||||
// 移除所有球体
|
||||
for (const ballId of this.activeBalls.keys()) {
|
||||
this.physicsEngine.removeBody(ballId);
|
||||
this.eventBus.emit('ball.removed', { id: ballId });
|
||||
}
|
||||
|
||||
this.activeBalls.clear();
|
||||
this.ballCount = 0;
|
||||
|
||||
console.log('[StandaloneMode] 游戏重置完成');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "f1a5b7ea-5782-43d3-9eea-8b5909a93dbd",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client-cocos/assets/scripts/Modules/Pinball/Input.meta
Normal file
9
client-cocos/assets/scripts/Modules/Pinball/Input.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "2121ed3d-9094-4dde-97cb-7ba9081cbd9d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* 输入管理器实现
|
||||
* 处理鼠标和触摸输入,转换为游戏事件
|
||||
*/
|
||||
|
||||
import { Camera, Component, EventMouse, EventTouch, Vec2, Vec3, _decorator } from 'cc';
|
||||
import { EventBus } from '../Core/EventBus';
|
||||
import { Vector2 } from '../Core/GameData';
|
||||
import {
|
||||
BaseInputEvent,
|
||||
InputButton,
|
||||
InputCallback,
|
||||
InputConfig,
|
||||
InputEventType,
|
||||
InputState,
|
||||
MouseInputEvent,
|
||||
TouchInputEvent
|
||||
} from './InputTypes';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass
|
||||
export class InputManager extends Component {
|
||||
|
||||
@property({ tooltip: '启用鼠标输入' })
|
||||
enableMouse: boolean = true;
|
||||
|
||||
@property({ tooltip: '启用触摸输入' })
|
||||
enableTouch: boolean = true;
|
||||
|
||||
@property({ tooltip: '双击时间间隔(ms)' })
|
||||
doubleClickTime: number = 300;
|
||||
|
||||
@property({ tooltip: '长按时间(ms)' })
|
||||
longPressTime: number = 500;
|
||||
|
||||
@property({ tooltip: '拖拽阈值(像素)' })
|
||||
dragThreshold: number = 10;
|
||||
|
||||
private eventBus: EventBus;
|
||||
private camera: Camera | null = null;
|
||||
private inputState: InputState;
|
||||
private config: InputConfig;
|
||||
private callbacks: Map<InputEventType, InputCallback[]> = new Map();
|
||||
|
||||
/**
|
||||
* 组件初始化
|
||||
*/
|
||||
onLoad(): void {
|
||||
this.eventBus = EventBus.getInstance();
|
||||
this.initializeInputState();
|
||||
this.initializeConfig();
|
||||
this.setupInputEvents();
|
||||
|
||||
console.log('InputManager initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化输入状态
|
||||
*/
|
||||
private initializeInputState(): void {
|
||||
this.inputState = {
|
||||
isMouseDown: false,
|
||||
isTouchActive: false,
|
||||
lastClickTime: 0,
|
||||
lastClickPosition: { x: 0, y: 0 },
|
||||
isDragging: false,
|
||||
dragStartPosition: { x: 0, y: 0 },
|
||||
activeTouches: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化配置
|
||||
*/
|
||||
private initializeConfig(): void {
|
||||
this.config = {
|
||||
enableMouse: this.enableMouse,
|
||||
enableTouch: this.enableTouch,
|
||||
doubleClickTime: this.doubleClickTime,
|
||||
longPressTime: this.longPressTime,
|
||||
dragThreshold: this.dragThreshold
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置相机引用
|
||||
*/
|
||||
setCamera(camera: Camera): void {
|
||||
this.camera = camera;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件回调
|
||||
*/
|
||||
on<T extends BaseInputEvent>(eventType: InputEventType, callback: InputCallback<T>): void {
|
||||
if (!this.callbacks.has(eventType)) {
|
||||
this.callbacks.set(eventType, []);
|
||||
}
|
||||
this.callbacks.get(eventType)!.push(callback as InputCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件回调
|
||||
*/
|
||||
off<T extends BaseInputEvent>(eventType: InputEventType, callback: InputCallback<T>): void {
|
||||
const callbacks = this.callbacks.get(eventType);
|
||||
if (callbacks) {
|
||||
const index = callbacks.indexOf(callback as InputCallback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置输入事件监听
|
||||
*/
|
||||
private setupInputEvents(): void {
|
||||
if (this.config.enableMouse) {
|
||||
this.node.on('mousedown', this.onMouseDown, this);
|
||||
this.node.on('mouseup', this.onMouseUp, this);
|
||||
this.node.on('mousemove', this.onMouseMove, this);
|
||||
}
|
||||
|
||||
if (this.config.enableTouch) {
|
||||
this.node.on('touchstart', this.onTouchStart, this);
|
||||
this.node.on('touchend', this.onTouchEnd, this);
|
||||
this.node.on('touchmove', this.onTouchMove, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标按下事件
|
||||
*/
|
||||
private onMouseDown(event: EventMouse): void {
|
||||
const position = this.screenToWorldPosition(event.getUILocation());
|
||||
const inputEvent: MouseInputEvent = {
|
||||
type: InputEventType.MOUSE_DOWN,
|
||||
position,
|
||||
screenPosition: { x: event.getUILocation().x, y: event.getUILocation().y },
|
||||
timestamp: Date.now(),
|
||||
button: event.getButton() as InputButton,
|
||||
ctrlKey: false, // Cocos Creator 3.x 中需要通过其他方式获取
|
||||
shiftKey: false,
|
||||
altKey: false
|
||||
};
|
||||
|
||||
this.inputState.isMouseDown = true;
|
||||
this.inputState.dragStartPosition = position;
|
||||
|
||||
// 检测双击
|
||||
const timeSinceLastClick = inputEvent.timestamp - this.inputState.lastClickTime;
|
||||
if (timeSinceLastClick < this.config.doubleClickTime) {
|
||||
const distance = this.calculateDistance(position, this.inputState.lastClickPosition);
|
||||
if (distance < this.config.dragThreshold) {
|
||||
this.emitDoubleClickEvent(inputEvent);
|
||||
}
|
||||
}
|
||||
|
||||
this.inputState.lastClickTime = inputEvent.timestamp;
|
||||
this.inputState.lastClickPosition = position;
|
||||
|
||||
this.emitEvent(inputEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标释放事件
|
||||
*/
|
||||
private onMouseUp(event: EventMouse): void {
|
||||
const position = this.screenToWorldPosition(event.getUILocation());
|
||||
const inputEvent: MouseInputEvent = {
|
||||
type: InputEventType.MOUSE_UP,
|
||||
position,
|
||||
screenPosition: { x: event.getUILocation().x, y: event.getUILocation().y },
|
||||
timestamp: Date.now(),
|
||||
button: event.getButton() as InputButton,
|
||||
ctrlKey: false,
|
||||
shiftKey: false,
|
||||
altKey: false
|
||||
};
|
||||
|
||||
this.inputState.isMouseDown = false;
|
||||
this.inputState.isDragging = false;
|
||||
|
||||
this.emitEvent(inputEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标移动事件
|
||||
*/
|
||||
private onMouseMove(event: EventMouse): void {
|
||||
const position = this.screenToWorldPosition(event.getUILocation());
|
||||
const inputEvent: MouseInputEvent = {
|
||||
type: InputEventType.MOUSE_MOVE,
|
||||
position,
|
||||
screenPosition: { x: event.getUILocation().x, y: event.getUILocation().y },
|
||||
timestamp: Date.now(),
|
||||
button: event.getButton() as InputButton,
|
||||
ctrlKey: false,
|
||||
shiftKey: false,
|
||||
altKey: false
|
||||
};
|
||||
|
||||
// 检测拖拽开始
|
||||
if (this.inputState.isMouseDown && !this.inputState.isDragging) {
|
||||
const distance = this.calculateDistance(position, this.inputState.dragStartPosition);
|
||||
if (distance > this.config.dragThreshold) {
|
||||
this.inputState.isDragging = true;
|
||||
this.emitDragStartEvent(inputEvent);
|
||||
}
|
||||
}
|
||||
|
||||
this.emitEvent(inputEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸开始事件
|
||||
*/
|
||||
private onTouchStart(event: EventTouch): void {
|
||||
const touches = event.getAllTouches();
|
||||
|
||||
for (const touch of touches) {
|
||||
const position = this.screenToWorldPosition(touch.getUILocation());
|
||||
const touchEvent: TouchInputEvent = {
|
||||
type: InputEventType.TOUCH_START,
|
||||
position,
|
||||
screenPosition: { x: touch.getUILocation().x, y: touch.getUILocation().y },
|
||||
timestamp: Date.now(),
|
||||
touchId: touch.getID(),
|
||||
force: undefined // Cocos Creator 3.x 中 getForce 方法可能不可用
|
||||
};
|
||||
|
||||
this.inputState.activeTouches.set(touch.getID(), touchEvent);
|
||||
this.inputState.isTouchActive = true;
|
||||
|
||||
this.emitEvent(touchEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸结束事件
|
||||
*/
|
||||
private onTouchEnd(event: EventTouch): void {
|
||||
const touches = event.getAllTouches();
|
||||
|
||||
for (const touch of touches) {
|
||||
const position = this.screenToWorldPosition(touch.getUILocation());
|
||||
const touchEvent: TouchInputEvent = {
|
||||
type: InputEventType.TOUCH_END,
|
||||
position,
|
||||
screenPosition: { x: touch.getUILocation().x, y: touch.getUILocation().y },
|
||||
timestamp: Date.now(),
|
||||
touchId: touch.getID(),
|
||||
force: undefined // Cocos Creator 3.x 中 getForce 方法可能不可用
|
||||
};
|
||||
|
||||
this.inputState.activeTouches.delete(touch.getID());
|
||||
if (this.inputState.activeTouches.size === 0) {
|
||||
this.inputState.isTouchActive = false;
|
||||
}
|
||||
|
||||
this.emitEvent(touchEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸移动事件
|
||||
*/
|
||||
private onTouchMove(event: EventTouch): void {
|
||||
const touches = event.getAllTouches();
|
||||
|
||||
for (const touch of touches) {
|
||||
const position = this.screenToWorldPosition(touch.getUILocation());
|
||||
const touchEvent: TouchInputEvent = {
|
||||
type: InputEventType.TOUCH_MOVE,
|
||||
position,
|
||||
screenPosition: { x: touch.getUILocation().x, y: touch.getUILocation().y },
|
||||
timestamp: Date.now(),
|
||||
touchId: touch.getID(),
|
||||
force: undefined // Cocos Creator 3.x 中 getForce 方法可能不可用
|
||||
};
|
||||
|
||||
this.emitEvent(touchEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 屏幕坐标转世界坐标
|
||||
*/
|
||||
private screenToWorldPosition(screenPos: Vec2): Vector2 {
|
||||
if (!this.camera) {
|
||||
// 如果没有相机,使用简单的坐标转换
|
||||
return {
|
||||
x: (screenPos.x - 400) / 100, // 假设屏幕中心为 (400, 300),缩放 100倍
|
||||
y: (300 - screenPos.y) / 100
|
||||
};
|
||||
}
|
||||
|
||||
// 使用相机进行坐标转换 - 转换为 Vec3
|
||||
const screenPos3 = new Vec3(screenPos.x, screenPos.y, 0);
|
||||
const worldPos = this.camera.screenToWorld(screenPos3);
|
||||
return {
|
||||
x: worldPos.x / 100, // 转换为物理世界坐标
|
||||
y: worldPos.y / 100
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点距离
|
||||
*/
|
||||
private calculateDistance(pos1: Vector2, pos2: Vector2): number {
|
||||
const dx = pos1.x - pos2.x;
|
||||
const dy = pos1.y - pos2.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发射事件
|
||||
*/
|
||||
private emitEvent(event: BaseInputEvent): void {
|
||||
const callbacks = this.callbacks.get(event.type);
|
||||
if (callbacks) {
|
||||
callbacks.forEach(callback => {
|
||||
try {
|
||||
callback(event);
|
||||
} catch (error) {
|
||||
console.error(`Error in input callback for ${event.type}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 同时通过事件总线发射
|
||||
this.eventBus.emit(event.type, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发射双击事件
|
||||
*/
|
||||
private emitDoubleClickEvent(event: MouseInputEvent): void {
|
||||
this.eventBus.emit('double_click', event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发射拖拽开始事件
|
||||
*/
|
||||
private emitDragStartEvent(event: MouseInputEvent): void {
|
||||
this.eventBus.emit('drag_start', event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前输入状态
|
||||
*/
|
||||
getInputState(): InputState {
|
||||
return { ...this.inputState };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件总线
|
||||
*/
|
||||
getEventBus(): EventBus {
|
||||
return this.eventBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
onDestroy(): void {
|
||||
// 移除事件监听
|
||||
this.node.off('mousedown', this.onMouseDown, this);
|
||||
this.node.off('mouseup', this.onMouseUp, this);
|
||||
this.node.off('mousemove', this.onMouseMove, this);
|
||||
this.node.off('touchstart', this.onTouchStart, this);
|
||||
this.node.off('touchend', this.onTouchEnd, this);
|
||||
this.node.off('touchmove', this.onTouchMove, this);
|
||||
|
||||
// 清理回调
|
||||
this.callbacks.clear();
|
||||
|
||||
// 清理事件总线
|
||||
this.eventBus.clear();
|
||||
|
||||
console.log('InputManager destroyed');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "1a3cf9f0-ddcf-4175-9dc8-632e89c6941e",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 输入相关类型定义
|
||||
*/
|
||||
|
||||
import { Vector2 } from '../Core/GameData';
|
||||
|
||||
/** 输入事件类型 */
|
||||
export enum InputEventType {
|
||||
MOUSE_DOWN = 'mouse_down',
|
||||
MOUSE_UP = 'mouse_up',
|
||||
MOUSE_MOVE = 'mouse_move',
|
||||
TOUCH_START = 'touch_start',
|
||||
TOUCH_END = 'touch_end',
|
||||
TOUCH_MOVE = 'touch_move'
|
||||
}
|
||||
|
||||
/** 输入按键枚举 */
|
||||
export enum InputButton {
|
||||
LEFT_MOUSE = 0,
|
||||
RIGHT_MOUSE = 1,
|
||||
MIDDLE_MOUSE = 2,
|
||||
TOUCH = 99
|
||||
}
|
||||
|
||||
/** 基础输入事件数据 */
|
||||
export interface BaseInputEvent {
|
||||
type: InputEventType;
|
||||
position: Vector2; // 世界坐标
|
||||
screenPosition: Vector2; // 屏幕坐标
|
||||
timestamp: number;
|
||||
button?: InputButton;
|
||||
}
|
||||
|
||||
/** 鼠标输入事件 */
|
||||
export interface MouseInputEvent extends BaseInputEvent {
|
||||
type: InputEventType.MOUSE_DOWN | InputEventType.MOUSE_UP | InputEventType.MOUSE_MOVE;
|
||||
button: InputButton;
|
||||
ctrlKey: boolean;
|
||||
shiftKey: boolean;
|
||||
altKey: boolean;
|
||||
}
|
||||
|
||||
/** 触摸输入事件 */
|
||||
export interface TouchInputEvent extends BaseInputEvent {
|
||||
type: InputEventType.TOUCH_START | InputEventType.TOUCH_END | InputEventType.TOUCH_MOVE;
|
||||
touchId: number;
|
||||
force?: number;
|
||||
}
|
||||
|
||||
/** 输入配置 */
|
||||
export interface InputConfig {
|
||||
enableMouse: boolean;
|
||||
enableTouch: boolean;
|
||||
doubleClickTime: number; // 双击时间间隔 (ms)
|
||||
longPressTime: number; // 长按时间 (ms)
|
||||
dragThreshold: number; // 拖拽阈值 (像素)
|
||||
}
|
||||
|
||||
/** 输入回调函数类型 */
|
||||
export type InputCallback<T extends BaseInputEvent = BaseInputEvent> = (event: T) => void;
|
||||
|
||||
/** 输入状态 */
|
||||
export interface InputState {
|
||||
isMouseDown: boolean;
|
||||
isTouchActive: boolean;
|
||||
lastClickTime: number;
|
||||
lastClickPosition: Vector2;
|
||||
isDragging: boolean;
|
||||
dragStartPosition: Vector2;
|
||||
activeTouches: Map<number, TouchInputEvent>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b07a5b72-454a-4fb1-8dd6-9b39141b85d8",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client-cocos/assets/scripts/Modules/Pinball/Network.meta
Normal file
9
client-cocos/assets/scripts/Modules/Pinball/Network.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "3cc0736e-7d1c-4771-b861-bfd3525e90d1",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client-cocos/assets/scripts/Modules/Pinball/Physics.meta
Normal file
9
client-cocos/assets/scripts/Modules/Pinball/Physics.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "0d17157f-5edf-479c-9296-4008cd31be83",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 物理引擎相关类型定义
|
||||
*/
|
||||
|
||||
import { Vector2 } from '../Core/GameData';
|
||||
|
||||
/** WASM 函数签名定义 */
|
||||
export interface WasmExports {
|
||||
// 世界管理
|
||||
pinball_create_world(gravity_x: number, gravity_y: number): number;
|
||||
pinball_step_world(world_id: number): void;
|
||||
|
||||
// 刚体管理
|
||||
pinball_create_dynamic_body(world_id: number, x: number, y: number): number;
|
||||
|
||||
// 位置获取
|
||||
pinball_get_body_x(world_id: number, body_id: number): number;
|
||||
pinball_get_body_y(world_id: number, body_id: number): number;
|
||||
|
||||
// 内存管理
|
||||
memory: WebAssembly.Memory;
|
||||
}
|
||||
|
||||
/** WASM 模块状态 */
|
||||
export enum WasmModuleState {
|
||||
UNLOADED = 'unloaded',
|
||||
LOADING = 'loading',
|
||||
LOADED = 'loaded',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
/** WASM 配置 */
|
||||
export interface WasmConfig {
|
||||
wasmPath: string;
|
||||
memoryPages?: number;
|
||||
importObject?: WebAssembly.Imports;
|
||||
}
|
||||
|
||||
/** 物理世界配置 */
|
||||
export interface PhysicsWorldConfig {
|
||||
gravity: Vector2;
|
||||
timeStep: number;
|
||||
velocityIterations: number;
|
||||
positionIterations: number;
|
||||
}
|
||||
|
||||
/** 碰撞体类型 */
|
||||
export enum ColliderType {
|
||||
CIRCLE = 'circle',
|
||||
BOX = 'box'
|
||||
}
|
||||
|
||||
/** 碰撞体定义 */
|
||||
export interface ColliderDef {
|
||||
type: ColliderType;
|
||||
radius?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
offset?: Vector2;
|
||||
}
|
||||
|
||||
/** 刚体类型 */
|
||||
export enum BodyType {
|
||||
STATIC = 'static',
|
||||
DYNAMIC = 'dynamic',
|
||||
KINEMATIC = 'kinematic'
|
||||
}
|
||||
|
||||
/** 刚体定义 */
|
||||
export interface BodyDef {
|
||||
type: BodyType;
|
||||
position: Vector2;
|
||||
velocity?: Vector2;
|
||||
colliders: ColliderDef[];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "6a80be32-a5d9-4b97-ace6-ac2c417a91b3",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* WASM 物理引擎实现
|
||||
* 提供与 pinball-physics WASM 模块的接口
|
||||
*/
|
||||
|
||||
import { wasmLoader } from '../../../Utils/WasmLoader';
|
||||
import { BodyId, PhysicsBodyData, PhysicsSettings, Vector2, WorldId } from '../Core/GameData';
|
||||
import { IPhysicsEngine } from '../Core/IPhysicsEngine';
|
||||
import { WasmExports, WasmModuleState } from './PhysicsTypes';
|
||||
|
||||
export class WasmPhysicsEngine implements IPhysicsEngine {
|
||||
private wasmModule: WebAssembly.Module | null = null;
|
||||
private wasmInstance: WebAssembly.Instance | null = null;
|
||||
private wasmExports: WasmExports | null = null;
|
||||
private state: WasmModuleState = WasmModuleState.UNLOADED;
|
||||
private worlds: Map<WorldId, boolean> = new Map();
|
||||
private bodies: Map<WorldId, Map<BodyId, boolean>> = new Map();
|
||||
|
||||
/**
|
||||
* 初始化 WASM 物理引擎
|
||||
* @param settings 物理设置
|
||||
* @param wasmFactory WASM工厂函数(可选,推荐使用)
|
||||
*/
|
||||
async initialize(settings: PhysicsSettings, wasmFactory?: any): Promise<void> {
|
||||
if (this.state === WasmModuleState.LOADED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state = WasmModuleState.LOADING;
|
||||
|
||||
try {
|
||||
if (wasmFactory) {
|
||||
// 使用新的 WasmLoader(推荐方式)
|
||||
await this.initializeWithWasmLoader(wasmFactory);
|
||||
} else {
|
||||
// 回退到旧的加载方式(保持向后兼容)
|
||||
console.warn('使用旧的 WASM 加载方式,推荐提供 wasmFactory 参数使用 WasmLoader');
|
||||
await this.initializeLegacy();
|
||||
}
|
||||
|
||||
this.state = WasmModuleState.LOADED;
|
||||
console.log('WASM Physics Engine initialized successfully');
|
||||
|
||||
} catch (error) {
|
||||
this.state = WasmModuleState.ERROR;
|
||||
console.error('Failed to initialize WASM Physics Engine:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 WASM 导入对象
|
||||
*/
|
||||
private createImportObject(): WebAssembly.Imports {
|
||||
return {
|
||||
env: {
|
||||
// 内存管理
|
||||
memory: new WebAssembly.Memory({
|
||||
initial: 10,
|
||||
maximum: 100
|
||||
}),
|
||||
|
||||
// 日志函数
|
||||
console_log: (ptr: number, len: number) => {
|
||||
// 可以实现 WASM 的日志输出
|
||||
},
|
||||
|
||||
// 错误处理
|
||||
abort: (msg: number, file: number, line: number, col: number) => {
|
||||
console.error('WASM abort:', { msg, file, line, col });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 WasmLoader 初始化(推荐方式)
|
||||
*/
|
||||
private async initializeWithWasmLoader(wasmFactory: any): Promise<void> {
|
||||
// 初始化 WASM 加载器
|
||||
wasmLoader.initialize();
|
||||
|
||||
// 检查平台支持
|
||||
if (!wasmLoader.isWasmSupported()) {
|
||||
throw new Error('当前平台不支持 WASM,需要提供 ASM 回退选项');
|
||||
}
|
||||
|
||||
// 加载 WASM 模块
|
||||
// 注意:在实际使用时需要提供正确的 editorUuid
|
||||
const instance = await wasmLoader.loadSimpleWasm(
|
||||
wasmFactory,
|
||||
'pinball_physics.wasm',
|
||||
undefined, // editorUuid,需要在实际使用时提供
|
||||
'wasm' // bundleName
|
||||
);
|
||||
|
||||
this.wasmExports = instance as WasmExports;
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧的初始化方法(保持向后兼容)
|
||||
*/
|
||||
private async initializeLegacy(): Promise<void> {
|
||||
// 加载 WASM 文件
|
||||
const wasmPath = 'assets/wasm/pinball_physics.wasm';
|
||||
const wasmResponse = await fetch(wasmPath);
|
||||
|
||||
if (!wasmResponse.ok) {
|
||||
throw new Error(`Failed to fetch WASM file: ${wasmResponse.statusText}`);
|
||||
}
|
||||
|
||||
const wasmBytes = await wasmResponse.arrayBuffer();
|
||||
|
||||
// 编译 WASM 模块
|
||||
this.wasmModule = await WebAssembly.compile(wasmBytes);
|
||||
|
||||
// 创建实例
|
||||
const importObject = this.createImportObject();
|
||||
this.wasmInstance = await WebAssembly.instantiate(this.wasmModule, importObject);
|
||||
this.wasmExports = this.wasmInstance.exports as unknown as WasmExports;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 WASM 是否已加载
|
||||
*/
|
||||
private ensureLoaded(): void {
|
||||
if (this.state !== WasmModuleState.LOADED || !this.wasmExports) {
|
||||
throw new Error('WASM Physics Engine not loaded. Call initialize() first.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建物理世界
|
||||
*/
|
||||
async createWorld(gravity: Vector2): Promise<WorldId> {
|
||||
this.ensureLoaded();
|
||||
|
||||
const worldId = this.wasmExports!.pinball_create_world(gravity.x, gravity.y);
|
||||
this.worlds.set(worldId, true);
|
||||
this.bodies.set(worldId, new Map());
|
||||
|
||||
console.log(`Created physics world ${worldId} with gravity (${gravity.x}, ${gravity.y})`);
|
||||
return worldId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁物理世界
|
||||
*/
|
||||
async destroyWorld(worldId: WorldId): Promise<void> {
|
||||
if (this.worlds.has(worldId)) {
|
||||
this.worlds.delete(worldId);
|
||||
this.bodies.delete(worldId);
|
||||
console.log(`Destroyed physics world ${worldId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行物理步进
|
||||
*/
|
||||
async step(worldId: WorldId): Promise<void> {
|
||||
this.ensureLoaded();
|
||||
|
||||
if (!this.worlds.has(worldId)) {
|
||||
throw new Error(`World ${worldId} does not exist`);
|
||||
}
|
||||
|
||||
this.wasmExports!.pinball_step_world(worldId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建动态刚体
|
||||
*/
|
||||
async createDynamicBody(worldId: WorldId, position: Vector2, radius: number): Promise<BodyId> {
|
||||
this.ensureLoaded();
|
||||
|
||||
if (!this.worlds.has(worldId)) {
|
||||
throw new Error(`World ${worldId} does not exist`);
|
||||
}
|
||||
|
||||
const bodyId = this.wasmExports!.pinball_create_dynamic_body(worldId, position.x, position.y);
|
||||
|
||||
// 记录刚体
|
||||
const worldBodies = this.bodies.get(worldId)!;
|
||||
worldBodies.set(bodyId, true);
|
||||
|
||||
console.log(`Created dynamic body ${bodyId} at (${position.x}, ${position.y}) with radius ${radius}`);
|
||||
return bodyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建静态刚体(暂时使用动态刚体实现)
|
||||
*/
|
||||
async createStaticBody(worldId: WorldId, position: Vector2, radius: number): Promise<BodyId> {
|
||||
// 目前 WASM 中只有 pinball_create_dynamic_body,后续可以扩展
|
||||
return this.createDynamicBody(worldId, position, radius);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁刚体(目前 WASM 中没有此函数,仅从记录中移除)
|
||||
*/
|
||||
async destroyBody(worldId: WorldId, bodyId: BodyId): Promise<void> {
|
||||
const worldBodies = this.bodies.get(worldId);
|
||||
if (worldBodies && worldBodies.has(bodyId)) {
|
||||
worldBodies.delete(bodyId);
|
||||
console.log(`Removed body ${bodyId} from tracking (WASM destroy not implemented yet)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取刚体位置
|
||||
*/
|
||||
async getBodyPosition(worldId: WorldId, bodyId: BodyId): Promise<Vector2 | null> {
|
||||
this.ensureLoaded();
|
||||
|
||||
const worldBodies = this.bodies.get(worldId);
|
||||
if (!worldBodies || !worldBodies.has(bodyId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const x = this.wasmExports!.pinball_get_body_x(worldId, bodyId);
|
||||
const y = this.wasmExports!.pinball_get_body_y(worldId, bodyId);
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置刚体位置(目前 WASM 中没有此函数)
|
||||
*/
|
||||
async setBodyPosition(worldId: WorldId, bodyId: BodyId, position: Vector2): Promise<void> {
|
||||
// 目前 WASM 中没有 pinball_set_body_position 函数
|
||||
console.warn('setBodyPosition not implemented in WASM yet');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取刚体速度(目前 WASM 中没有此函数)
|
||||
*/
|
||||
async getBodyVelocity(worldId: WorldId, bodyId: BodyId): Promise<Vector2 | null> {
|
||||
// 目前 WASM 中没有速度获取函数
|
||||
console.warn('getBodyVelocity not implemented in WASM yet');
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置刚体速度(目前 WASM 中没有此函数)
|
||||
*/
|
||||
async setBodyVelocity(worldId: WorldId, bodyId: BodyId, velocity: Vector2): Promise<void> {
|
||||
// 目前 WASM 中没有 pinball_set_body_velocity 函数
|
||||
console.warn('setBodyVelocity not implemented in WASM yet');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有物理体数据
|
||||
*/
|
||||
async getAllBodies(worldId: WorldId): Promise<PhysicsBodyData[]> {
|
||||
const worldBodies = this.bodies.get(worldId);
|
||||
if (!worldBodies) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const bodiesData: PhysicsBodyData[] = [];
|
||||
|
||||
for (const bodyId of worldBodies.keys()) {
|
||||
const position = await this.getBodyPosition(worldId, bodyId);
|
||||
const velocity = await this.getBodyVelocity(worldId, bodyId);
|
||||
|
||||
if (position && velocity) {
|
||||
bodiesData.push({
|
||||
id: bodyId,
|
||||
position,
|
||||
velocity,
|
||||
rotation: 0,
|
||||
angularVelocity: 0,
|
||||
bodyType: 'circle',
|
||||
radius: 0.5, // 默认半径,可以后续改进
|
||||
isStatic: false // 可以根据需要区分静态和动态
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return bodiesData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建圆形刚体 (新接口方法)
|
||||
*/
|
||||
createCircle(options: import('../Core/IPhysicsEngine').CreateCircleOptions): BodyId {
|
||||
this.ensureLoaded();
|
||||
|
||||
// 由于目前使用第一个世界,获取第一个世界ID
|
||||
const firstWorldId = this.worlds.keys().next().value;
|
||||
if (firstWorldId === undefined) {
|
||||
throw new Error('No physics world created');
|
||||
}
|
||||
|
||||
// 目前WASM只支持动态刚体创建
|
||||
const bodyId = this.wasmExports!.pinball_create_dynamic_body(firstWorldId, options.position.x, options.position.y);
|
||||
|
||||
this.bodies.get(firstWorldId)!.set(bodyId, true);
|
||||
return bodyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建矩形刚体 (新接口方法)
|
||||
*/
|
||||
createBox(options: import('../Core/IPhysicsEngine').CreateBoxOptions): BodyId {
|
||||
this.ensureLoaded();
|
||||
|
||||
const firstWorldId = this.worlds.keys().next().value;
|
||||
if (firstWorldId === undefined) {
|
||||
throw new Error('No physics world created');
|
||||
}
|
||||
|
||||
// 目前WASM只支持动态刚体创建,矩形用动态刚体模拟
|
||||
const bodyId = this.wasmExports!.pinball_create_dynamic_body(firstWorldId, options.position.x, options.position.y);
|
||||
|
||||
this.bodies.get(firstWorldId)!.set(bodyId, true);
|
||||
return bodyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除刚体 (新接口方法)
|
||||
*/
|
||||
removeBody(bodyId: BodyId): void {
|
||||
// 查找包含此bodyId的世界
|
||||
for (const [worldId, worldBodies] of this.bodies) {
|
||||
if (worldBodies.has(bodyId)) {
|
||||
worldBodies.delete(bodyId);
|
||||
// 注意:这里需要WASM支持删除函数
|
||||
// this.wasmExports!.pinball_remove_body(worldId, bodyId);
|
||||
console.log(`Body ${bodyId} removed from world ${worldId}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取刚体数据 (新接口方法)
|
||||
*/
|
||||
getBodyData(bodyId: BodyId): PhysicsBodyData | null {
|
||||
// 查找包含此bodyId的世界
|
||||
for (const [worldId, worldBodies] of this.bodies) {
|
||||
if (worldBodies.has(bodyId)) {
|
||||
// 这里需要同步调用WASM函数获取数据
|
||||
try {
|
||||
const x = this.wasmExports!.pinball_get_body_x(worldId, bodyId);
|
||||
const y = this.wasmExports!.pinball_get_body_y(worldId, bodyId);
|
||||
// WASM暂时不支持速度获取,使用默认值
|
||||
const vx = 0;
|
||||
const vy = 0;
|
||||
|
||||
return {
|
||||
id: bodyId,
|
||||
position: { x, y },
|
||||
velocity: { x: vx, y: vy },
|
||||
rotation: 0, // 暂时固定值
|
||||
angularVelocity: 0, // 暂时固定值
|
||||
bodyType: 'circle',
|
||||
radius: 0.5, // 暂时固定值
|
||||
isStatic: false // 暂时固定值
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to get body data for body ${bodyId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源 (新接口方法)
|
||||
*/
|
||||
cleanup(): void {
|
||||
this.dispose(); // 异步转同步
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
async dispose(): Promise<void> {
|
||||
this.worlds.clear();
|
||||
this.bodies.clear();
|
||||
this.wasmInstance = null;
|
||||
this.wasmExports = null;
|
||||
this.wasmModule = null;
|
||||
this.state = WasmModuleState.UNLOADED;
|
||||
|
||||
console.log('WASM Physics Engine disposed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态
|
||||
*/
|
||||
getState(): WasmModuleState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查平台是否支持WASM(使用WasmLoader)
|
||||
*/
|
||||
isWasmSupported(): boolean {
|
||||
return wasmLoader.isWasmSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推荐的加载策略
|
||||
*/
|
||||
getRecommendedStrategy(): 'wasm' | 'asm' | 'unsupported' {
|
||||
return wasmLoader.getRecommendedStrategy();
|
||||
}
|
||||
}
|
||||
400
client-cocos/assets/scripts/Modules/Pinball/PinballManager.ts
Normal file
400
client-cocos/assets/scripts/Modules/Pinball/PinballManager.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
/**
|
||||
* PinballManager - Pinball模块主控制器
|
||||
* 负责整个Pinball模块的生命周期管理和模式切换
|
||||
*/
|
||||
|
||||
import { _decorator, Camera, Component, director, Director, game, Game, Node, Scene } from 'cc';
|
||||
import { EventBus } from './Core/EventBus';
|
||||
import { GameState, PinballConfig } from './Core/GameData';
|
||||
import { StandaloneMode } from './GameModes/StandaloneMode';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
export enum PinballMode {
|
||||
STANDALONE = 'standalone',
|
||||
CLIENT_MULTIPLAYER = 'client-multiplayer',
|
||||
SERVER_MULTIPLAYER = 'server-multiplayer'
|
||||
}
|
||||
|
||||
@ccclass('PinballManager')
|
||||
export class PinballManager extends Component {
|
||||
|
||||
@property({
|
||||
type: Node,
|
||||
tooltip: "主相机节点,用于渲染游戏画面"
|
||||
})
|
||||
cameraNode: Node = null;
|
||||
|
||||
@property({
|
||||
type: Node,
|
||||
tooltip: "渲染容器节点,所有游戏对象将在此节点下渲染"
|
||||
})
|
||||
renderContainer: Node = null;
|
||||
|
||||
@property({
|
||||
type: Node,
|
||||
tooltip: "UI容器节点,用于显示游戏UI界面"
|
||||
})
|
||||
uiContainer: Node = null;
|
||||
|
||||
@property({
|
||||
tooltip: "启动时的默认游戏模式"
|
||||
})
|
||||
defaultMode: PinballMode = PinballMode.STANDALONE;
|
||||
|
||||
@property({
|
||||
tooltip: "是否在启动时自动开始游戏"
|
||||
})
|
||||
autoStart: boolean = true;
|
||||
|
||||
@property({
|
||||
tooltip: "调试模式,启用详细日志"
|
||||
})
|
||||
debugMode: boolean = true;
|
||||
|
||||
// 核心系统
|
||||
private eventBus: EventBus = null;
|
||||
private currentMode: PinballMode = null;
|
||||
private currentGameMode: Component = null;
|
||||
private mainCamera: Camera = null;
|
||||
|
||||
// 游戏状态
|
||||
private isInitialized: boolean = false;
|
||||
private isPaused: boolean = false;
|
||||
private gameConfig: PinballConfig = null;
|
||||
|
||||
/**
|
||||
* 启动 PinballManager
|
||||
* 必须在配置应用后调用
|
||||
*/
|
||||
async Start(): Promise<boolean> {
|
||||
this.log('[PinballManager] Start 开始');
|
||||
|
||||
try {
|
||||
// 初始化事件总线
|
||||
this.eventBus = EventBus.getInstance();
|
||||
|
||||
// 验证必需的节点
|
||||
if (!this.validateRequiredNodes()) {
|
||||
console.error('[PinballManager] 必需节点验证失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取主相机
|
||||
this.initializeCamera();
|
||||
|
||||
// 创建默认配置
|
||||
this.createDefaultConfig();
|
||||
|
||||
// 注册全局事件监听器
|
||||
this.registerGlobalEvents();
|
||||
|
||||
// 初始化完成
|
||||
this.isInitialized = true;
|
||||
this.log('[PinballManager] 初始化完成');
|
||||
|
||||
this.log('[PinballManager] Start 完成');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[PinballManager] Start 过程中发生错误:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 PinballManager
|
||||
*/
|
||||
Stop(): void {
|
||||
this.log('[PinballManager] Stop 开始');
|
||||
this.cleanup();
|
||||
this.isInitialized = false;
|
||||
this.log('[PinballManager] Stop 完成');
|
||||
}
|
||||
|
||||
// 保留 onDestroy 作为安全清理
|
||||
onDestroy() {
|
||||
this.Stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证必需的节点
|
||||
*/
|
||||
private validateRequiredNodes(): boolean {
|
||||
if (!this.cameraNode) {
|
||||
console.error('[PinballManager] cameraNode 未设置');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.renderContainer) {
|
||||
console.error('[PinballManager] renderContainer 未设置');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化相机
|
||||
*/
|
||||
private initializeCamera(): void {
|
||||
this.mainCamera = this.cameraNode.getComponent(Camera);
|
||||
if (!this.mainCamera) {
|
||||
console.error('[PinballManager] 主相机组件未找到');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log('主相机初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认配置
|
||||
*/
|
||||
private createDefaultConfig(): void {
|
||||
this.gameConfig = {
|
||||
mode: this.defaultMode,
|
||||
physicsSettings: {
|
||||
gravity: { x: 0, y: -9.81 },
|
||||
timeStep: 1 / 60
|
||||
},
|
||||
renderSettings: {
|
||||
enableEffects: true,
|
||||
maxParticles: 500
|
||||
},
|
||||
wasmPath: 'assets/wasm/pinball_physics.wasm'
|
||||
};
|
||||
|
||||
this.log('默认配置创建完成', this.gameConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册全局事件监听器
|
||||
*/
|
||||
private registerGlobalEvents(): void {
|
||||
// 监听场景切换事件 - Cocos Creator 3.x 中使用静态常量
|
||||
director.on(Director.EVENT_BEFORE_SCENE_LAUNCH, this.onSceneChange, this);
|
||||
|
||||
// 监听游戏暂停/恢复 - Cocos Creator 3.x 中使用game事件
|
||||
game.on(Game.EVENT_HIDE, this.onGamePause, this);
|
||||
game.on(Game.EVENT_SHOW, this.onGameResume, this);
|
||||
|
||||
this.log('全局事件监听器注册完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动游戏
|
||||
*/
|
||||
public async startGame(mode: PinballMode): Promise<boolean> {
|
||||
if (!this.isInitialized) {
|
||||
console.error('[PinballManager] 尚未初始化');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.currentGameMode) {
|
||||
await this.stopCurrentGame();
|
||||
}
|
||||
|
||||
this.log(`开始启动游戏模式: ${mode}`);
|
||||
|
||||
try {
|
||||
switch (mode) {
|
||||
case PinballMode.STANDALONE:
|
||||
await this.startStandaloneMode();
|
||||
break;
|
||||
|
||||
case PinballMode.CLIENT_MULTIPLAYER:
|
||||
console.warn('[PinballManager] Client Multiplayer 模式尚未实现');
|
||||
return false;
|
||||
|
||||
case PinballMode.SERVER_MULTIPLAYER:
|
||||
console.warn('[PinballManager] Server Multiplayer 模式尚未实现');
|
||||
return false;
|
||||
|
||||
default:
|
||||
console.error(`[PinballManager] 未知游戏模式: ${mode}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.currentMode = mode;
|
||||
this.gameConfig.mode = mode;
|
||||
|
||||
this.log(`游戏模式 ${mode} 启动成功`);
|
||||
|
||||
// 发送游戏开始事件
|
||||
this.eventBus.emit('game.started', { mode });
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[PinballManager] 启动游戏模式 ${mode} 失败:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动Standalone模式
|
||||
*/
|
||||
private async startStandaloneMode(): Promise<void> {
|
||||
// 添加StandaloneMode组件
|
||||
const standaloneMode = this.node.addComponent(StandaloneMode);
|
||||
|
||||
// 设置所需的引用
|
||||
standaloneMode.gameCamera = this.mainCamera;
|
||||
standaloneMode.renderNode = this.renderContainer;
|
||||
standaloneMode.boundsNode = this.renderContainer; // 简化设置
|
||||
|
||||
// StandaloneMode 会在 onLoad 中自动初始化
|
||||
this.currentGameMode = standaloneMode;
|
||||
|
||||
this.log('Standalone模式启动完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止当前游戏模式
|
||||
*/
|
||||
public async stopCurrentGame(): Promise<void> {
|
||||
if (!this.currentGameMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(`停止当前游戏模式: ${this.currentMode}`);
|
||||
|
||||
// 销毁当前游戏模式组件
|
||||
this.currentGameMode.destroy();
|
||||
this.currentGameMode = null;
|
||||
this.currentMode = null;
|
||||
|
||||
// 发送游戏停止事件
|
||||
this.eventBus.emit('game.stopped', {});
|
||||
|
||||
this.log('当前游戏模式已停止');
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停游戏
|
||||
*/
|
||||
public pauseGame(): void {
|
||||
if (this.isPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isPaused = true;
|
||||
this.eventBus.emit('game.paused', {});
|
||||
this.log('游戏已暂停');
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复游戏
|
||||
*/
|
||||
public resumeGame(): void {
|
||||
if (!this.isPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isPaused = false;
|
||||
this.eventBus.emit('game.resumed', {});
|
||||
this.log('游戏已恢复');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启当前游戏
|
||||
*/
|
||||
public async restartGame(): Promise<boolean> {
|
||||
const currentMode = this.currentMode;
|
||||
|
||||
if (currentMode) {
|
||||
await this.stopCurrentGame();
|
||||
return await this.startGame(currentMode);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换游戏模式
|
||||
*/
|
||||
public async switchMode(newMode: PinballMode): Promise<boolean> {
|
||||
if (this.currentMode === newMode) {
|
||||
this.log(`已经是 ${newMode} 模式`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return await this.startGame(newMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景切换事件处理
|
||||
*/
|
||||
private onSceneChange(scene: Scene): void {
|
||||
this.log('场景切换事件');
|
||||
// 可以在此处理场景切换时的清理工作
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏暂停事件处理
|
||||
*/
|
||||
private onGamePause(): void {
|
||||
this.pauseGame();
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏恢复事件处理
|
||||
*/
|
||||
private onGameResume(): void {
|
||||
this.resumeGame();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
private cleanup(): void {
|
||||
// 清理全局事件监听器
|
||||
director.off(Director.EVENT_BEFORE_SCENE_LAUNCH, this.onSceneChange, this);
|
||||
game.off(Game.EVENT_HIDE, this.onGamePause, this);
|
||||
game.off(Game.EVENT_SHOW, this.onGameResume, this);
|
||||
|
||||
// 停止当前游戏
|
||||
if (this.currentGameMode) {
|
||||
this.stopCurrentGame();
|
||||
}
|
||||
|
||||
this.log('PinballManager 资源清理完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前游戏状态
|
||||
*/
|
||||
public getGameState(): GameState | null {
|
||||
if (this.currentGameMode && this.currentGameMode instanceof StandaloneMode) {
|
||||
return this.currentGameMode.getGameStats() as any;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取游戏配置
|
||||
*/
|
||||
public getConfig(): PinballConfig {
|
||||
return this.gameConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新游戏配置
|
||||
*/
|
||||
public updateConfig(newConfig: Partial<PinballConfig>): void {
|
||||
this.gameConfig = { ...this.gameConfig, ...newConfig };
|
||||
this.log('游戏配置已更新', this.gameConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试日志输出
|
||||
*/
|
||||
private log(message: string, data?: any): void {
|
||||
if (this.debugMode) {
|
||||
if (data) {
|
||||
console.log(`[PinballManager] ${message}`, data);
|
||||
} else {
|
||||
console.log(`[PinballManager] ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "03de2ced-4b54-4dac-8773-b078be08a33f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "27270c02-6c7f-448a-922d-12d7734844de",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* Pinball 渲染器实现
|
||||
* 负责渲染物理对象、粒子效果等
|
||||
*/
|
||||
|
||||
import { _decorator, Camera, Color, Component, Graphics, Node, Prefab, Sprite, tween, Vec3 } from 'cc';
|
||||
import { PhysicsBodyData, Vector2 } from '../Core/GameData';
|
||||
import { IRenderer, ParticleEffect, RenderObject } from '../Core/IRenderer';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
|
||||
@ccclass
|
||||
export class PinballRenderer extends Component implements IRenderer {
|
||||
|
||||
@property(Node)
|
||||
private renderContainer: Node = null!;
|
||||
|
||||
@property(Prefab)
|
||||
private ballPrefab: Prefab = null!;
|
||||
|
||||
private renderObjects: Map<string, Node> = new Map();
|
||||
private particlePool: Node[] = [];
|
||||
private camera: Camera = null!;
|
||||
|
||||
/**
|
||||
* 设置相机
|
||||
*/
|
||||
setCamera(camera: Camera): void {
|
||||
this.camera = camera;
|
||||
console.log('Camera set for PinballRenderer');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置世界边界
|
||||
*/
|
||||
setWorldBounds(width: number, height: number): void {
|
||||
console.log(`World bounds set to ${width} x ${height}`);
|
||||
// 可以在此处设置渲染边界或背景
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化渲染器
|
||||
*/
|
||||
async initialize(parentNode: any): Promise<void> {
|
||||
console.log('Initializing PinballRenderer...');
|
||||
|
||||
// 设置渲染容器
|
||||
if (parentNode) {
|
||||
this.renderContainer = parentNode;
|
||||
} else {
|
||||
this.renderContainer = this.node;
|
||||
}
|
||||
|
||||
// 获取相机 - 在 Cocos Creator 3.x 中需要通过场景查找
|
||||
const cameraNode = this.renderContainer.scene?.getChildByName('Main Camera');
|
||||
if (cameraNode) {
|
||||
this.camera = cameraNode.getComponent(Camera);
|
||||
}
|
||||
|
||||
if (!this.camera) {
|
||||
console.warn('Main camera not found, creating default camera');
|
||||
const cameraNode = new Node('PinballCamera');
|
||||
this.camera = cameraNode.addComponent(Camera);
|
||||
this.renderContainer.addChild(cameraNode);
|
||||
}
|
||||
|
||||
// 创建默认球体预制件(如果没有提供)
|
||||
if (!this.ballPrefab) {
|
||||
await this.createDefaultBallPrefab();
|
||||
}
|
||||
|
||||
console.log('PinballRenderer initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认的球体预制件
|
||||
*/
|
||||
private async createDefaultBallPrefab(): Promise<void> {
|
||||
// 创建一个简单的圆形节点作为默认球体
|
||||
const ballNode = new Node('DefaultBall');
|
||||
|
||||
// 添加 Sprite 组件
|
||||
const sprite = ballNode.addComponent(Sprite);
|
||||
|
||||
// 创建圆形材质
|
||||
const graphics = ballNode.addComponent(Graphics);
|
||||
graphics.fillColor = Color.WHITE;
|
||||
graphics.circle(0, 0, 25); // 半径 25 像素
|
||||
graphics.fill();
|
||||
|
||||
console.log('Created default ball prefab');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染物理体
|
||||
*/
|
||||
renderBodies(bodies: PhysicsBodyData[]): void {
|
||||
// 清理不存在的渲染对象
|
||||
const currentBodyIds = new Set(bodies.map(body => body.id.toString()));
|
||||
for (const [id, node] of this.renderObjects) {
|
||||
if (!currentBodyIds.has(id)) {
|
||||
this.removeRenderObject(id);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新或创建渲染对象
|
||||
for (const body of bodies) {
|
||||
const bodyIdStr = body.id.toString();
|
||||
let renderNode = this.renderObjects.get(bodyIdStr);
|
||||
|
||||
if (!renderNode) {
|
||||
// 创建新的渲染对象
|
||||
const renderObject = this.createRenderObject(body);
|
||||
renderNode = this.createRenderNode(renderObject);
|
||||
this.renderObjects.set(bodyIdStr, renderNode);
|
||||
this.renderContainer.addChild(renderNode);
|
||||
}
|
||||
|
||||
// 更新位置
|
||||
renderNode.setPosition(body.position.x * 100, body.position.y * 100); // 放大显示
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建渲染对象数据
|
||||
*/
|
||||
createRenderObject(body: PhysicsBodyData): RenderObject {
|
||||
return {
|
||||
id: body.id.toString(),
|
||||
position: body.position,
|
||||
radius: body.radius,
|
||||
color: body.isStatic ?
|
||||
{ r: 128, g: 128, b: 128, a: 255 } : // 静态物体:灰色
|
||||
{ r: 255, g: 100, b: 100, a: 255 }, // 动态物体:红色
|
||||
layer: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建渲染节点
|
||||
*/
|
||||
private createRenderNode(renderObject: RenderObject): Node {
|
||||
const node = new Node(`Ball_${renderObject.id}`);
|
||||
|
||||
// 添加 Graphics 组件用于绘制圆形
|
||||
const graphics = node.addComponent(Graphics);
|
||||
graphics.fillColor = new Color(
|
||||
renderObject.color.r,
|
||||
renderObject.color.g,
|
||||
renderObject.color.b,
|
||||
renderObject.color.a
|
||||
);
|
||||
|
||||
const radius = Math.max(renderObject.radius * 100, 10); // 最小半径 10 像素
|
||||
graphics.circle(0, 0, radius);
|
||||
graphics.fill();
|
||||
|
||||
// 添加边框
|
||||
graphics.strokeColor = Color.BLACK;
|
||||
graphics.lineWidth = 2;
|
||||
graphics.circle(0, 0, radius);
|
||||
graphics.stroke();
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新渲染对象
|
||||
*/
|
||||
updateRenderObject(renderObject: RenderObject, body: PhysicsBodyData): void {
|
||||
const node = this.renderObjects.get(renderObject.id);
|
||||
if (node) {
|
||||
// 更新位置
|
||||
node.setPosition(body.position.x * 100, body.position.y * 100);
|
||||
|
||||
// 可以在此添加更多属性的更新,如颜色、大小等
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除渲染对象
|
||||
*/
|
||||
removeRenderObject(bodyId: string): void {
|
||||
const node = this.renderObjects.get(bodyId);
|
||||
if (node) {
|
||||
node.removeFromParent();
|
||||
this.renderObjects.delete(bodyId);
|
||||
console.log(`Removed render object ${bodyId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放粒子效果
|
||||
*/
|
||||
playParticleEffect(effect: ParticleEffect): void {
|
||||
// 创建简单的粒子效果节点
|
||||
const particleNode = this.getParticleNode();
|
||||
particleNode.setPosition(effect.position.x * 100, effect.position.y * 100);
|
||||
|
||||
// 使用 Cocos Creator 3.x 的 tween 系统
|
||||
tween(particleNode)
|
||||
.parallel(
|
||||
tween().to(effect.lifetime, { opacity: 0 }),
|
||||
tween().by(effect.lifetime, {
|
||||
position: new Vec3(effect.velocity.x * 50, effect.velocity.y * 50, 0)
|
||||
})
|
||||
)
|
||||
.call(() => {
|
||||
this.recycleParticleNode(particleNode);
|
||||
})
|
||||
.start();
|
||||
|
||||
this.renderContainer.addChild(particleNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取粒子节点(对象池)
|
||||
*/
|
||||
private getParticleNode(): Node {
|
||||
if (this.particlePool.length > 0) {
|
||||
return this.particlePool.pop()!;
|
||||
}
|
||||
|
||||
const node = new Node('Particle');
|
||||
const graphics = node.addComponent(Graphics);
|
||||
graphics.fillColor = Color.YELLOW;
|
||||
graphics.circle(0, 0, 3);
|
||||
graphics.fill();
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回收粒子节点
|
||||
*/
|
||||
private recycleParticleNode(node: Node): void {
|
||||
node.removeFromParent();
|
||||
// 重置透明度 - 在 Cocos Creator 3.x 中通过 UIOpacity 组件
|
||||
const uiOpacity = node.getComponent('cc.UIOpacity') as any;
|
||||
if (uiOpacity) {
|
||||
uiOpacity.opacity = 255;
|
||||
}
|
||||
node.setPosition(0, 0, 0); // 重置位置
|
||||
this.particlePool.push(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有渲染对象
|
||||
*/
|
||||
clear(): void {
|
||||
for (const [id, node] of this.renderObjects) {
|
||||
node.removeFromParent();
|
||||
}
|
||||
this.renderObjects.clear();
|
||||
|
||||
// 清理粒子池
|
||||
for (const particle of this.particlePool) {
|
||||
particle.removeFromParent();
|
||||
}
|
||||
this.particlePool = [];
|
||||
|
||||
console.log('Cleared all render objects');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置相机位置
|
||||
*/
|
||||
setCameraPosition(position: Vector2): void {
|
||||
if (this.camera) {
|
||||
this.camera.node.setPosition(position.x * 100, position.y * 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置相机缩放
|
||||
*/
|
||||
setCameraZoom(zoom: number): void {
|
||||
if (this.camera) {
|
||||
// 在 Cocos Creator 3.x 中使用 orthoHeight 控制缩放
|
||||
this.camera.orthoHeight = 600 / zoom; // 基础高度 600
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁渲染器
|
||||
*/
|
||||
dispose(): void {
|
||||
this.clear();
|
||||
console.log('PinballRenderer disposed');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.23",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "da8038e2-7c90-4e3e-a3f6-4f8b88b3a11d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user