From d3669bd44e2910c318ae26d27d9adad6d7ac1a50 Mon Sep 17 00:00:00 2001 From: chicrobot Date: Wed, 4 Feb 2026 21:48:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(SS)=20=E6=B7=BB=E5=8A=A0=E5=9C=B0=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/InputPlugin架构设计.md | 196 ++++++++++++++ scenes/storeScenes/BreatheLife.js | 30 ++- scenes/storeScenes/WorldLand.js | 164 ++++++------ scenes/storeScenes/WorldTime.js | 18 +- src/index.html | 34 +++ src/main.js | 46 ++++ src/plugins/InputPlugin.js | 113 ++++++++ src/plugins/adapters/CocosInputAdapter.js | 74 ++++++ src/plugins/adapters/IInputAdapter.js | 90 +++++++ src/plugins/adapters/PhaserInputAdapter.js | 288 +++++++++++++++++++++ src/plugins/core/InputSystem.js | 116 +++++++++ src/scenes/GameScene.js | 74 ++++++ src/scenes/JoystickTestScene.js | 215 +++++++++++++++ 13 files changed, 1359 insertions(+), 99 deletions(-) create mode 100644 docs/InputPlugin架构设计.md create mode 100644 src/index.html create mode 100644 src/main.js create mode 100644 src/plugins/InputPlugin.js create mode 100644 src/plugins/adapters/CocosInputAdapter.js create mode 100644 src/plugins/adapters/IInputAdapter.js create mode 100644 src/plugins/adapters/PhaserInputAdapter.js create mode 100644 src/plugins/core/InputSystem.js create mode 100644 src/scenes/GameScene.js create mode 100644 src/scenes/JoystickTestScene.js diff --git a/docs/InputPlugin架构设计.md b/docs/InputPlugin架构设计.md new file mode 100644 index 0000000..47da689 --- /dev/null +++ b/docs/InputPlugin架构设计.md @@ -0,0 +1,196 @@ +# InputPlugin 架构设计说明 + +## 问题分析 + +### 原始设计的问题 +1. **紧耦合**:直接依赖 Phaser 的 Key 对象和事件系统 +2. **无法跨引擎**:切换引擎需要重写整个插件 +3. **违反开闭原则**:修改输入方式需要修改插件内部代码 +4. **测试困难**:难以进行单元测试(依赖具体引擎) + +## 解决方案:适配器模式 + +### 架构层次 + +``` +┌─────────────────────────────────────┐ +│ 业务逻辑层 (GameScene) │ +│ - 不关心底层实现 │ +│ - 只调用统一接口 │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ InputPlugin (核心插件) │ +│ - 提供统一接口 │ +│ - 管理适配器生命周期 │ +│ - registerKey(), isDown(), isUp() │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ IInputAdapter (接口定义) │ +│ - 定义适配器必须实现的方法 │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ 具体适配器实现 │ +│ - PhaserInputAdapter │ +│ - PixiInputAdapter │ +│ - CocosInputAdapter │ +│ - GodotInputAdapter (可扩展) │ +└─────────────────────────────────────┘ +``` + +### 核心优势 + +#### 1. **跨引擎复用** +```javascript +// Phaser 中使用 +const phaserPlugin = new InputPlugin(scene); +phaserPlugin.registerKey('W', ['ArrowUp']); +if (phaserPlugin.isDown('W')) { /* 移动 */ } + +// PixiJS 中使用(代码完全相同) +const pixiPlugin = new PixiInputPlugin(app); +pixiPlugin.registerKey('W', ['ArrowUp']); +if (pixiPlugin.isDown('W')) { /* 移动 */ } +``` + +#### 2. **易于扩展** +添加新引擎支持只需实现适配器接口: +```javascript +class GodotInputAdapter extends IInputAdapter { + initialize() { /* Godot 特定实现 */ } + registerKey(key, aliases) { /* ... */ } + isDown(key) { /* ... */ } + isUp(key) { /* ... */ } + destroy() { /* ... */ } +} +``` + +#### 3. **易于测试** +```javascript +class MockInputAdapter extends IInputAdapter { + constructor() { + super(); + this.mockKeys = new Map(); + } + + initialize() {} + + registerKey(key, aliases) { + this.mockKeys.set(key, false); + } + + isDown(key) { + return this.mockKeys.get(key) || false; + } + + isUp(key) { + return !this.isDown(key); + } + + destroy() {} + + // 测试辅助方法 + setKeyDown(key) { + this.mockKeys.set(key, true); + } + + setKeyUp(key) { + this.mockKeys.set(key, false); + } +} + +// 单元测试示例 +const mockAdapter = new MockInputAdapter(); +const plugin = new InputPlugin(null, null); +plugin.inputAdapter = mockAdapter; + +plugin.registerKey('W'); +mockAdapter.setKeyDown('W'); +assert(plugin.isDown('W') === true); +``` + +#### 4. **符合设计原则** +- **单一职责原则**:每个适配器只负责一个引擎的输入处理 +- **开闭原则**:对扩展开放,对修改关闭 +- **依赖倒置原则**:依赖抽象接口,不依赖具体实现 +- **里氏替换原则**:所有适配器可以互相替换 + +## 使用示例 + +### Phaser 项目 +```javascript +// main.js +import { InputPlugin } from './plugins/InputPlugin.js'; + +const config = { + scene: [GameScene], + plugins: { + scene: [ + { + key: 'InputPlugin', + plugin: InputPlugin, + mapping: 'inputPlugin' + } + ] + } +}; + +// GameScene.js +create() { + // 注册按键 + this.inputPlugin.registerKey('W', ['ArrowUp']); + this.inputPlugin.registerKey('S', ['ArrowDown']); + this.inputPlugin.registerKey('A', ['ArrowLeft']); + this.inputPlugin.registerKey('D', ['ArrowRight']); +} + +update() { + // 使用统一接口 + if (this.inputPlugin.isDown('W')) { + this.player.setVelocityY(-200); + } + if (this.inputPlugin.isDown('S')) { + this.player.setVelocityY(200); + } + // ... +} +``` + +### PixiJS 项目 +```javascript +// main.js +import { PixiInputPlugin } from './plugins/PixiInputPlugin.js'; + +const app = new PIXI.Application({ width: 800, height: 600 }); +const inputPlugin = new PixiInputPlugin(app); + +inputPlugin.registerKey('W', ['ArrowUp']); +inputPlugin.registerKey('S', ['ArrowDown']); + +// 游戏循环 +app.ticker.add(() => { + if (inputPlugin.isDown('W')) { + player.y -= 2; + } + if (inputPlugin.isDown('S')) { + player.y += 2; + } +}); +``` + +## 总结 + +**适配器模式的优势:** +- ✅ 解耦:业务逻辑与具体引擎实现分离 +- ✅ 复用:同一套业务逻辑可在不同引擎中使用 +- ✅ 扩展:添加新引擎支持无需修改现有代码 +- ✅ 测试:可以轻松创建 Mock 对象进行单元测试 +- ✅ 维护:每个适配器独立维护,降低复杂度 + +**何时使用适配器模式:** +- 需要集成第三方库或框架 +- 需要支持多种实现方式 +- 需要在不修改现有代码的情况下扩展功能 +- 需要提高代码的可测试性 diff --git a/scenes/storeScenes/BreatheLife.js b/scenes/storeScenes/BreatheLife.js index 30624c3..b94a2a3 100644 --- a/scenes/storeScenes/BreatheLife.js +++ b/scenes/storeScenes/BreatheLife.js @@ -5,17 +5,34 @@ * 当创建的角色超过3个时,点击已存在的角色会为其设置纹理 * 当设置纹理的角色超过3个时,自动切换到 MenuScene 场景 */ -export default class BreatheLife extends Phaser.Scene { +import WorldLand from './WorldLand.js' +export default class BreatheLife extends WorldLand { /** * 构造函数 - * 初始化场景并设置场景键名为 'BreatheLife' + * 初始化场景并设置场景键名 + * @param {string} sceneKey - 场景键名,默认值为 'BreatheLife' */ - constructor() { - super('BreatheLife') + constructor(sceneKey = 'BreatheLife') { + super(sceneKey) this.nextScene = 'MenuScene' + + this.UIDepth = 100 // UI层级深度,确保在其他元素之上 + this.maskDepth = 99 // 黑夜遮罩层级,低于UI层级 + this.SpriteDepth = 98 // 角色层级,确保在地图层级之上 + this.mapDepth = 97 // 地图层级,确保在UI层级之下 + + + this.tileCount = 234 // 瓦片总数,从tile_0000到tile_0233 + this.tileSize = 16 // 瓦片大小 + this.mapWidth = 18; // 瓦片图行数 + this.mapHeight = 13; // 瓦片图列数 } preload() { + // 加载草地瓦片图 + this.load.image(`grass`, `assets/PNG/Tiles/Tiles/tile_0024.png`) + this.load.image(`smallGrass`, `assets/PNG/Tiles/Tiles/tile_0044.png`) + // 加载角色动画和图片资源 this.load.image(`player0-0`, `assets/PNG/Players/Tiles/tile_${String(0).padStart(4, '0')}.png`) this.load.image(`player1-0`, `assets/PNG/Players/Tiles/tile_${String(8).padStart(4, '0')}.png`) @@ -47,6 +64,10 @@ export default class BreatheLife extends Phaser.Scene { this.spriteAnimCount = 0 this.targetCount = 3 // 目标角色数量 + this.createTilemap() // 创建瓦片地图 + this.initFrameRateText() // 初始化帧率显示文本 + this.initDayNight(this.toGameTime('2000-01-01 12:00:00')) // 初始化时间,白天黑夜状态 + // 监听鼠标点击事件,点击后创建一个新的角色 this.input.on('pointerdown', () => { // 获取点击位置的坐标 @@ -84,6 +105,7 @@ export default class BreatheLife extends Phaser.Scene { }else if (this.spriteCount < 3) { // 创建一个新的角色 sprite,缩放为1倍,并设置原点为中心底部 const sprite = this.physics.add.sprite(x, y, 'player').setScale(1).setOrigin(0.5, 1) + sprite.setDepth(this.SpriteDepth) // 设置角色层级 // 设置角色碰撞世界边界 sprite.setCollideWorldBounds(true) diff --git a/scenes/storeScenes/WorldLand.js b/scenes/storeScenes/WorldLand.js index eb625c0..03cdaa3 100644 --- a/scenes/storeScenes/WorldLand.js +++ b/scenes/storeScenes/WorldLand.js @@ -4,44 +4,71 @@ * 初始引导场景,显示空白背景并在用户交互或自动延迟后 * 执行过渡动画切换到下一个场景 */ -import GameTimeManager from '../../systems/GameTimeManager.js' +import WorldTime from './WorldTime.js' -export default class WorldLand extends Phaser.Scene { +export default class WorldLand extends WorldTime { /** * 构造函数 - * 初始化场景并设置场景键名为 'WorldLand' + * 初始化场景并设置场景键名 + * @param {string} sceneKey - 场景键名 */ - constructor() { - super('WorldLand') + constructor(sceneKey = 'WorldLand') { + super(sceneKey) this.nextScene = 'BreatheLife' - this.timeManager = new GameTimeManager() // 创建游戏时间管理器实例 - this.UIDepth = 100 // UI层级深度,确保在其他元素之上 this.maskDepth = 99 // 黑夜遮罩层级,低于UI层级 + this.mapDepth = 98 // 地图层级,确保在UI层级之下 + this.tileCount = 234 // 瓦片总数,从tile_0000到tile_0233 + this.tileSize = 16 // 瓦片大小 + this.mapWidth = 18; // 瓦片图行数 + this.mapHeight = 13; // 瓦片图列数 + } + preload() { + // 加载草地瓦片图 + this.load.image(`grass`, `assets/PNG/Tiles/Tiles/tile_0024.png`) + this.load.image(`smallGrass`, `assets/PNG/Tiles/Tiles/tile_0044.png`) } create() { // 设置场景背景颜色为深灰色 this.cameras.main.setBackgroundColor('#999999') + // 初始化计数器,用于跟踪创建的小草数量 + this.smallGrassCount = 0 - // 监听鼠标点击事件,点击后执行场景过渡动画 - this.input.on('pointerdown', () => { - this.scene.start(this.nextScene); - }) + this.initInput() // 初始化输入事件 + this.initDayNight(this.toGameTime('2000-01-01 17:57:00')) // 初始化时间,白天黑夜状态 + + this.createTilemap() // 创建瓦片地图 - // 监听键盘按键事件,按键后执行场景过渡动画 - this.input.keyboard.on('keydown', () => { - this.scene.start(this.nextScene); - }) - - this.initDayNight(this.toGameTime('1999-12-30 23:57:00')) // 初始化时间,白天黑夜状态 this.initFrameRateText() // 初始化帧率显示文本 } - /** - * 更新游戏时间 - * 每帧调用,刷新游戏时间 - */ + + createTilemap() { + // 获取当前屏幕大小 + const screenWidth = this.cameras.main.width; + const screenHeight = this.cameras.main.height; + + // 计算需要的瓦片数量 + const tilesX = Math.ceil(screenWidth / this.tileSize); + const tilesY = Math.ceil(screenHeight / this.tileSize); + + // 草地铺满屏幕 + for (let y = 0; y < tilesY; y++) { + for (let x = 0; x < tilesX; x++) { + // 使用草地瓦片 + const tile = this.add.image( + x * this.tileSize + this.tileSize / 2, + y * this.tileSize + this.tileSize / 2, + 'grass' + ); + tile.setDepth(this.mapDepth); // 设置深度,确保在UI层级之下 + } + } + + console.log(`草地铺满屏幕完成,使用了 ${tilesX}x${tilesY} 个瓦片`) + } + update() { this.showFrameRate() // 显示当前帧率 this.updateGameTime() // 更新游戏时间 @@ -49,7 +76,31 @@ export default class WorldLand extends Phaser.Scene { this.updateDayNight() // 更新昼夜状态 } + initInput() { + // 监听鼠标点击事件,点击后创建一个新的小草 + this.input.on('pointerdown', () => { + // 获取点击位置的坐标 + const { x, y } = this.input.activePointer + this.createTile(x, y) + }) + + // 监听键盘按键事件,按键后执行场景过渡动画 + this.input.keyboard.on('keydown', () => { + this.scene.start(this.nextScene); + }) + } + createTile(x, y) { + if (this.smallGrassCount >= 10) { + this.scene.start(this.nextScene); + } + // 创建瓦片图实例 + const tile = this.add.image(x, y, 'smallGrass'); + tile.setDepth(this.mapDepth); // 设置深度,确保在UI层级之下 + tile.setScrollFactor(0); // 固定瓦片,不随相机移动 + // 增加小草计数器 + this.smallGrassCount++; + } initFrameRateText() { // 创建帧率显示文本 this.frameRateText = this.add.text( @@ -61,74 +112,7 @@ export default class WorldLand extends Phaser.Scene { ).setScrollFactor(0).setDepth(this.UIDepth) // 固定文本,不随相机移动,UI层级 } showFrameRate() { - // 获取当前帧率 - const frameRate = this.game.loop.actualFps - // 更新帧率显示文本 - this.frameRateText.setText(`Frame Rate: ${frameRate.toFixed(2)}`) - } - /** - * 初始化昼夜状态 - * 设置初始为白天,添加事件监听切换状态 - */ - initDayNight(gameTime) { - this.timeManager.init(gameTime) // 初始化时间管理器 - // 创建时间显示文本 - this.timeText = this.add.text( - 20, 20, '', { - fontSize: '20px', - fill: '#ffffff', - fontFamily: 'Arial' - } - ).setScrollFactor(0).setDepth(this.UIDepth) // 固定文本,不随相机移动,UI层级 - // 添加黑夜遮罩 - this.nightMask = this.add.rectangle( - 0, 0, - this.cameras.main.width, - this.cameras.main.height, - 0x000000 - ).setOrigin(0, 0).setScrollFactor(0).setDepth(this.maskDepth) // 固定遮罩,不随相机移动,遮罩层级 - this.nightMask.alpha = 0 // 初始设置为透明 - } - /** - * 更新游戏时间 - * 每帧调用,基于现实时间流逝更新游戏时间 - */ - updateGameTime() { - this.timeManager.updateGameTime() // 使用时间管理器更新游戏时间 - } - /** - * 获取游戏中的日期 - * 根据游戏时间戳计算游戏年月日时分秒,格式为YYYY-MM-DD HH:mm:ss - */ - getGameDate() { - return this.timeManager.getGameDate() // 使用时间管理器获取游戏日期 - } - /** - * 转换游戏时间字符串为游戏时间戳 - * 接收时间字符串,格式为YYYY-MM-DD HH:mm:ss,返回游戏时间戳(单位:秒) - */ - toGameTime(gameTimeString) { - return this.timeManager.toGameTime(gameTimeString) // 使用时间管理器转换游戏时间 - } - /** - * 显示当前时间 - * 每帧调用,更新显示时间 - */ - displayTime() { - const timeString = this.getGameDate().dateString - const day = this.timeManager.getDayNightState() ? '白天' : '黑夜' - // 更新显示时间 - this.timeText.setText(`${day} ${timeString}`) - } - /** - * 更新昼夜状态 - * 每帧调用,根据游戏时间切换白天黑夜状态 - */ - updateDayNight() { - const isDay = this.timeManager.updateDayNight() - // 根据昼夜状态更新遮罩 - if (this.nightMask) { - this.nightMask.alpha = isDay ? 0 : 0.5 - } + const frameRate = this.game.loop.actualFps // 获取当前帧率 + this.frameRateText.setText(`Frame Rate: ${frameRate.toFixed(2)}`) // 更新帧率显示文本 } } diff --git a/scenes/storeScenes/WorldTime.js b/scenes/storeScenes/WorldTime.js index 7b53eaf..9b8978f 100644 --- a/scenes/storeScenes/WorldTime.js +++ b/scenes/storeScenes/WorldTime.js @@ -7,10 +7,11 @@ export default class WorldTime extends Phaser.Scene { /** * 构造函数 - * 初始化场景并设置场景键名为 'WorldTime' + * 初始化场景并设置场景键名 + * @param {string} sceneKey - 场景键名 */ - constructor() { - super('WorldTime') + constructor(sceneKey = 'WorldTime') { + super(sceneKey) this.nextScene = 'WorldLand' this.gameTime = 0 // 游戏中的时间戳(单位:秒) @@ -34,6 +35,15 @@ export default class WorldTime extends Phaser.Scene { // 设置场景背景颜色为深灰色 this.cameras.main.setBackgroundColor('#999999') + this.initInput() // 初始化输入事件 + + this.initDayNight(this.toGameTime('1999-12-30 23:57:00')) // 初始化时间,白天黑夜状态 + } + /** + * 初始化输入事件 + * 监听鼠标点击和键盘按键,触发场景过渡 + */ + initInput() { // 监听鼠标点击事件,点击后执行场景过渡动画 this.input.on('pointerdown', () => { this.scene.start(this.nextScene); @@ -43,8 +53,6 @@ export default class WorldTime extends Phaser.Scene { this.input.keyboard.on('keydown', () => { this.scene.start(this.nextScene); }) - - this.initDayNight(this.toGameTime('2000-01-01 17:57:00')) // 初始化时间,白天黑夜状态 } /** * 更新游戏时间 diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..ec56a85 --- /dev/null +++ b/src/index.html @@ -0,0 +1,34 @@ + + + + + + Phaser + + + + + + + diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..038edf2 --- /dev/null +++ b/src/main.js @@ -0,0 +1,46 @@ +// main.js + +import { GameScene } from './scenes/GameScene.js'; +import { JoystickTestScene } from './scenes/JoystickTestScene.js'; +import { InputPlugin } from './plugins/InputPlugin.js'; + +console.log('InputPlugin 导入:', InputPlugin); + +const config = { + type: Phaser.AUTO, + width: 800, + height: 600, + backgroundColor: '#000000', + physics: { + default: 'arcade', + arcade: { + debug: false + } + }, + scene: [JoystickTestScene, GameScene], + plugins: { + scene: [ + { + key: 'InputPlugin', + plugin: InputPlugin, + mapping: 'inputPlugin' + } + ] + }, + // 添加缩放配置,解决拉伸问题 + scale: { + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_BOTH, + min: { + width: 400, + height: 300 + }, + max: { + width: 1600, + height: 1200 + } + } +}; + +console.log('游戏配置:', config); +window.game = new Phaser.Game(config); diff --git a/src/plugins/InputPlugin.js b/src/plugins/InputPlugin.js new file mode 100644 index 0000000..a0f1999 --- /dev/null +++ b/src/plugins/InputPlugin.js @@ -0,0 +1,113 @@ +// plugins/InputPlugin.js + +import { InputSystem } from './core/InputSystem.js'; +import { PhaserInputAdapter } from './adapters/PhaserInputAdapter.js'; + +/** + * InputPlugin 场景插件 + * 作为 Phaser 场景插件的包装器,使用适配器架构 + */ +export class InputPlugin extends Phaser.Plugins.ScenePlugin { + constructor(scene, pluginManager) { + super(scene, pluginManager); + console.log('InputPlugin 场景插件初始化'); + + // 创建 Phaser 适配器 + this.adapter = new PhaserInputAdapter(scene); + + // 创建核心输入系统 + this.inputSystem = new InputSystem(this.adapter); + + // 初始化 + this.inputSystem.initialize(); + } + + /** + * 启动插件 + */ + boot() { + console.log('InputPlugin 场景插件启动'); + } + + /** + * 测试方法 + * @returns {string} 测试结果 + */ + testMethod() { + return 'InputPlugin 测试方法'; + } + + /** + * 创建触摸轮盘 + * @param {Object} config 轮盘配置 + * @returns {string} 轮盘ID + */ + createJoystick(config) { + return this.inputSystem.createJoystick(config); + } + + /** + * 创建屏幕按钮 + * @param {Object} config 按钮配置 + * @returns {string} 按钮ID + */ + createButton(config) { + return this.inputSystem.createButton(config); + } + + /** + * 获取轮盘方向向量 + * @param {string} joystickId 轮盘ID + * @returns {Object} 方向向量 {x, y} + */ + getJoystickDirection(joystickId) { + return this.inputSystem.getJoystickDirection(joystickId); + } + + /** + * 获取轮盘力度 + * @param {string} joystickId 轮盘ID + * @returns {number} 力度值 (0-1) + */ + getJoystickMagnitude(joystickId) { + return this.inputSystem.getJoystickMagnitude(joystickId); + } + + /** + * 获取归一化的轮盘方向 + * @param {string} joystickId 轮盘ID + * @returns {Object} 归一化向量 {x, y} (-1 到 1) + */ + getJoystickNormalized(joystickId) { + return this.inputSystem.getJoystickNormalized(joystickId); + } + + /** + * 检查按钮是否按下 + * @param {string} buttonId 按钮ID + * @returns {boolean} 是否按下 + */ + isButtonDown(buttonId) { + return this.inputSystem.isButtonDown(buttonId); + } + + /** + * 检查按钮是否释放 + * @param {string} buttonId 按钮ID + * @returns {boolean} 是否释放 + */ + isButtonUp(buttonId) { + return this.inputSystem.isButtonUp(buttonId); + } + + /** + * 销毁插件 + */ + destroy() { + console.log('InputPlugin 场景插件销毁'); + if (this.inputSystem) { + this.inputSystem.destroy(); + } + super.destroy(); + } +} diff --git a/src/plugins/adapters/CocosInputAdapter.js b/src/plugins/adapters/CocosInputAdapter.js new file mode 100644 index 0000000..c31f2ed --- /dev/null +++ b/src/plugins/adapters/CocosInputAdapter.js @@ -0,0 +1,74 @@ +// plugins/adapters/CocosInputAdapter.js + +export class CocosInputAdapter { + constructor(director) { + this.director = director; + this.keyMap = new Map(); + this.isInitialized = false; + } + + initialize() { + if (this.isInitialized) return; + + cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, (event) => { + this.handleKeyDown(event); + }); + + cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, (event) => { + this.handleKeyUp(event); + }); + + this.isInitialized = true; + console.log('CocosInputAdapter 已初始化'); + } + + handleKeyDown(event) { + const keyCode = event.keyCode; + this.keyState.set(keyCode, true); + } + + handleKeyUp(event) { + const keyCode = event.keyCode; + this.keyState.set(keyCode, false); + } + + registerKey(key, aliases = []) { + const keyCode = this.getKeyCode(key); + this.keyMap.set(key, keyCode); + + aliases.forEach(alias => { + const aliasKeyCode = this.getKeyCode(alias); + this.keyMap.set(alias, keyCode); + }); + } + + getKeyCode(key) { + const keyMap = { + 'W': cc.macro.KEY.w, + 'A': cc.macro.KEY.a, + 'S': cc.macro.KEY.s, + 'D': cc.macro.KEY.d, + 'UP': cc.macro.KEY.up, + 'DOWN': cc.macro.KEY.down, + 'LEFT': cc.macro.KEY.left, + 'RIGHT': cc.macro.KEY.right + }; + return keyMap[key] || key; + } + + isDown(key) { + const keyCode = this.keyMap.get(key); + return this.keyState.get(keyCode) || false; + } + + isUp(key) { + return !this.isDown(key); + } + + destroy() { + cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN); + cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP); + this.keyMap.clear(); + this.keyState.clear(); + } +} diff --git a/src/plugins/adapters/IInputAdapter.js b/src/plugins/adapters/IInputAdapter.js new file mode 100644 index 0000000..8806bed --- /dev/null +++ b/src/plugins/adapters/IInputAdapter.js @@ -0,0 +1,90 @@ +// plugins/adapters/IInputAdapter.js + +/** + * 输入适配器接口 + * 所有引擎适配器必须实现这些方法 + */ +export class IInputAdapter { + constructor() { + if (this.constructor === IInputAdapter) { + throw new Error('IInputAdapter 是抽象类,不能直接实例化'); + } + } + + /** + * 初始化适配器 + */ + initialize() { + throw new Error('initialize() 必须由子类实现'); + } + + /** + * 创建触摸轮盘 + * @param {Object} config 轮盘配置 + * @returns {Object} 轮盘实例 + */ + createJoystick(config) { + throw new Error('createJoystick() 必须由子类实现'); + } + + /** + * 创建屏幕按钮 + * @param {Object} config 按钮配置 + * @returns {Object} 按钮实例 + */ + createButton(config) { + throw new Error('createButton() 必须由子类实现'); + } + + /** + * 获取轮盘方向向量 + * @param {string} joystickId 轮盘ID + * @returns {Object} 方向向量 {x, y} + */ + getJoystickDirection(joystickId) { + throw new Error('getJoystickDirection() 必须由子类实现'); + } + + /** + * 获取轮盘力度 + * @param {string} joystickId 轮盘ID + * @returns {number} 力度值 (0-1) + */ + getJoystickMagnitude(joystickId) { + throw new Error('getJoystickMagnitude() 必须由子类实现'); + } + + /** + * 获取归一化的轮盘方向 + * @param {string} joystickId 轮盘ID + * @returns {Object} 归一化向量 {x, y} (-1 到 1) + */ + getJoystickNormalized(joystickId) { + throw new Error('getJoystickNormalized() 必须由子类实现'); + } + + /** + * 检查按钮是否按下 + * @param {string} buttonId 按钮ID + * @returns {boolean} 是否按下 + */ + isButtonDown(buttonId) { + throw new Error('isButtonDown() 必须由子类实现'); + } + + /** + * 检查按钮是否释放 + * @param {string} buttonId 按钮ID + * @returns {boolean} 是否释放 + */ + isButtonUp(buttonId) { + throw new Error('isButtonUp() 必须由子类实现'); + } + + /** + * 销毁适配器 + */ + destroy() { + throw new Error('destroy() 必须由子类实现'); + } +} diff --git a/src/plugins/adapters/PhaserInputAdapter.js b/src/plugins/adapters/PhaserInputAdapter.js new file mode 100644 index 0000000..6940f87 --- /dev/null +++ b/src/plugins/adapters/PhaserInputAdapter.js @@ -0,0 +1,288 @@ +// plugins/adapters/PhaserInputAdapter.js + +import { IInputAdapter } from './IInputAdapter.js'; + +/** + * Phaser 输入适配器 + * 实现 IInputAdapter 接口,使用 Phaser API + */ +export class PhaserInputAdapter extends IInputAdapter { + constructor(scene) { + super(); + this.scene = scene; + this.joysticks = new Map(); + this.buttons = new Map(); + console.log('PhaserInputAdapter 初始化'); + } + + /** + * 初始化适配器 + */ + initialize() { + if (!this.scene.input) { + console.warn('Phaser 输入系统不可用'); + } + console.log('PhaserInputAdapter 已初始化'); + } + + /** + * 创建触摸轮盘 + * @param {Object} config 轮盘配置 + * @returns {Object} 轮盘实例 + */ + createJoystick(config) { + try { + const joystick = { + id: config.id, + base: null, + thumb: null, + isActive: false, + startX: config.x || 100, + startY: config.y || this.scene.sys.game.config.height - 100, + radius: config.radius || 60, + maxDistance: config.maxDistance || 40, + currentX: 0, + currentY: 0, + normalizedX: 0, + normalizedY: 0, + magnitude: 0 + }; + + // 创建轮盘底座 + joystick.base = this.scene.add.circle(joystick.startX, joystick.startY, joystick.radius, 0x444444, 0.5); + joystick.base.setScrollFactor(0); + + // 创建轮盘拇指 + joystick.thumb = this.scene.add.circle(joystick.startX, joystick.startY, joystick.radius * 0.5, 0x888888, 0.8); + joystick.thumb.setScrollFactor(0); + + // 监听触摸事件 + const self = this; + this.scene.input.on('pointerdown', function(pointer) { + if (self.calculateDistance(pointer.x, pointer.y, joystick.startX, joystick.startY) < joystick.radius) { + joystick.isActive = true; + joystick.thumb.setPosition(pointer.x, pointer.y); + self.updateJoystick(joystick, pointer); + } + }); + + this.scene.input.on('pointermove', function(pointer) { + if (joystick.isActive) { + self.updateJoystick(joystick, pointer); + } + }); + + this.scene.input.on('pointerup', function() { + if (joystick.isActive) { + joystick.isActive = false; + joystick.thumb.setPosition(joystick.startX, joystick.startY); + joystick.currentX = 0; + joystick.currentY = 0; + joystick.normalizedX = 0; + joystick.normalizedY = 0; + joystick.magnitude = 0; + } + }); + + this.joysticks.set(config.id, joystick); + return joystick; + } catch (error) { + console.error('创建轮盘失败:', error); + return null; + } + } + + /** + * 创建屏幕按钮 + * @param {Object} config 按钮配置 + * @returns {Object} 按钮实例 + */ + createButton(config) { + try { + const button = { + id: config.id, + sprite: null, + text: null, + isDown: false, + x: config.x || this.scene.sys.game.config.width - 100, + y: config.y || this.scene.sys.game.config.height - 100, + width: config.width || 80, + height: config.height || 80, + label: config.label || 'Button', + onDown: config.onDown || null, + onUp: config.onUp || null + }; + + // 创建按钮背景 + button.sprite = this.scene.add.rectangle(button.x, button.y, button.width, button.height, 0x444444, 0.5); + button.sprite.setScrollFactor(0); + button.sprite.setInteractive(); + + // 创建按钮文字 + button.text = this.scene.add.text(button.x, button.y, button.label, { + fill: '#ffffff', + fontSize: '16px', + fontFamily: 'Arial' + }); + button.text.setScrollFactor(0); + button.text.setOrigin(0.5, 0.5); + + // 监听触摸事件 + button.sprite.on('pointerdown', function() { + button.isDown = true; + button.sprite.setFillStyle(0x666666, 0.8); + if (button.onDown) { + button.onDown(); + } + }); + + button.sprite.on('pointerup', function() { + button.isDown = false; + button.sprite.setFillStyle(0x444444, 0.5); + if (button.onUp) { + button.onUp(); + } + }); + + button.sprite.on('pointerout', function() { + if (button.isDown) { + button.isDown = false; + button.sprite.setFillStyle(0x444444, 0.5); + } + }); + + this.buttons.set(config.id, button); + return button; + } catch (error) { + console.error('创建按钮失败:', error); + return null; + } + } + + /** + * 计算两点之间的距离 + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @returns {number} 距离 + */ + calculateDistance(x1, y1, x2, y2) { + const dx = x2 - x1; + const dy = y2 - y1; + return Math.sqrt(dx * dx + dy * dy); + } + + /** + * 计算两点之间的角度 + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @returns {number} 角度 + */ + calculateAngle(x1, y1, x2, y2) { + return Math.atan2(y2 - y1, x2 - x1); + } + + /** + * 更新轮盘状态 + * @param {Object} joystick 轮盘实例 + * @param {Object} pointer 指针事件 + */ + updateJoystick(joystick, pointer) { + const distance = this.calculateDistance(joystick.startX, joystick.startY, pointer.x, pointer.y); + const angle = this.calculateAngle(joystick.startX, joystick.startY, pointer.x, pointer.y); + + // 限制最大距离 + const limitedDistance = Math.min(distance, joystick.maxDistance); + joystick.magnitude = limitedDistance / joystick.maxDistance; + + // 计算位置 + joystick.currentX = Math.cos(angle) * limitedDistance; + joystick.currentY = Math.sin(angle) * limitedDistance; + + // 归一化(-1 到 1 之间) + joystick.normalizedX = joystick.currentX / joystick.maxDistance; + joystick.normalizedY = joystick.currentY / joystick.maxDistance; + + // 更新拇指位置 + joystick.thumb.setPosition( + joystick.startX + joystick.currentX, + joystick.startY + joystick.currentY + ); + } + + /** + * 获取轮盘方向向量 + * @param {string} joystickId 轮盘ID + * @returns {Object} 方向向量 {x, y} + */ + getJoystickDirection(joystickId) { + const joystick = this.joysticks.get(joystickId); + if (!joystick) return { x: 0, y: 0 }; + return { x: joystick.currentX, y: joystick.currentY }; + } + + /** + * 获取轮盘力度 + * @param {string} joystickId 轮盘ID + * @returns {number} 力度值 (0-1) + */ + getJoystickMagnitude(joystickId) { + const joystick = this.joysticks.get(joystickId); + if (!joystick) return 0; + return joystick.magnitude; + } + + /** + * 获取归一化的轮盘方向 + * @param {string} joystickId 轮盘ID + * @returns {Object} 归一化向量 {x, y} (-1 到 1) + */ + getJoystickNormalized(joystickId) { + const joystick = this.joysticks.get(joystickId); + if (!joystick) return { x: 0, y: 0 }; + return { x: joystick.normalizedX, y: joystick.normalizedY }; + } + + /** + * 检查按钮是否按下 + * @param {string} buttonId 按钮ID + * @returns {boolean} 是否按下 + */ + isButtonDown(buttonId) { + const button = this.buttons.get(buttonId); + return button ? button.isDown : false; + } + + /** + * 检查按钮是否释放 + * @param {string} buttonId 按钮ID + * @returns {boolean} 是否释放 + */ + isButtonUp(buttonId) { + return !this.isButtonDown(buttonId); + } + + /** + * 销毁适配器 + */ + destroy() { + // 清理轮盘 + this.joysticks.forEach(joystick => { + if (joystick.base) joystick.base.destroy(); + if (joystick.thumb) joystick.thumb.destroy(); + }); + this.joysticks.clear(); + + // 清理按钮 + this.buttons.forEach(button => { + if (button.sprite) button.sprite.destroy(); + if (button.text) button.text.destroy(); + }); + this.buttons.clear(); + + console.log('PhaserInputAdapter 已销毁'); + } +} diff --git a/src/plugins/core/InputSystem.js b/src/plugins/core/InputSystem.js new file mode 100644 index 0000000..1276388 --- /dev/null +++ b/src/plugins/core/InputSystem.js @@ -0,0 +1,116 @@ +// plugins/core/InputSystem.js + +/** + * 核心输入系统 - 引擎无关 + * 使用适配器模式实现跨引擎兼容 + */ +export class InputSystem { + constructor(adapter) { + if (!adapter) { + throw new Error('InputSystem 需要一个适配器实例'); + } + this.adapter = adapter; + this.joysticks = new Map(); + this.buttons = new Map(); + console.log('InputSystem 初始化'); + } + + /** + * 初始化输入系统 + */ + initialize() { + this.adapter.initialize(); + console.log('InputSystem 已初始化'); + } + + /** + * 创建触摸轮盘 + * @param {Object} config 轮盘配置 + * @returns {string} 轮盘ID + */ + createJoystick(config) { + const joystickId = config.id || `joystick_${this.joysticks.size}`; + const createdJoystick = this.adapter.createJoystick({ + ...config, + id: joystickId + }); + if (createdJoystick) { + this.joysticks.set(joystickId, createdJoystick); + console.log('创建轮盘:', joystickId); + } + return joystickId; + } + + /** + * 创建屏幕按钮 + * @param {Object} config 按钮配置 + * @returns {string} 按钮ID + */ + createButton(config) { + const buttonId = config.id || `button_${this.buttons.size}`; + const createdButton = this.adapter.createButton({ + ...config, + id: buttonId + }); + if (createdButton) { + this.buttons.set(buttonId, createdButton); + console.log('创建按钮:', buttonId); + } + return buttonId; + } + + /** + * 获取轮盘方向向量 + * @param {string} joystickId 轮盘ID + * @returns {Object} 方向向量 {x, y} + */ + getJoystickDirection(joystickId) { + return this.adapter.getJoystickDirection(joystickId); + } + + /** + * 获取轮盘力度 + * @param {string} joystickId 轮盘ID + * @returns {number} 力度值 (0-1) + */ + getJoystickMagnitude(joystickId) { + return this.adapter.getJoystickMagnitude(joystickId); + } + + /** + * 获取归一化的轮盘方向 + * @param {string} joystickId 轮盘ID + * @returns {Object} 归一化向量 {x, y} (-1 到 1) + */ + getJoystickNormalized(joystickId) { + return this.adapter.getJoystickNormalized(joystickId); + } + + /** + * 检查按钮是否按下 + * @param {string} buttonId 按钮ID + * @returns {boolean} 是否按下 + */ + isButtonDown(buttonId) { + return this.adapter.isButtonDown(buttonId); + } + + /** + * 检查按钮是否释放 + * @param {string} buttonId 按钮ID + * @returns {boolean} 是否释放 + */ + isButtonUp(buttonId) { + return this.adapter.isButtonUp(buttonId); + } + + /** + * 销毁输入系统 + */ + destroy() { + this.adapter.destroy(); + this.joysticks.clear(); + this.buttons.clear(); + console.log('InputSystem 已销毁'); + } +} diff --git a/src/scenes/GameScene.js b/src/scenes/GameScene.js new file mode 100644 index 0000000..e8473a3 --- /dev/null +++ b/src/scenes/GameScene.js @@ -0,0 +1,74 @@ +// scenes/GameScene.js + +export class GameScene extends Phaser.Scene { + constructor() { + super('GameScene'); + this.moveSpeed = 200; + this.joystickId = null; + this.inputPlugin = null; + } + + preload() { + } + + create() { + const graphics = this.make.graphics(); + graphics.fillStyle(0xffffff, 1); + graphics.fillRect(0, 0, 32, 32); + graphics.generateTexture('player', 32, 32); + graphics.destroy(); + + this.player = this.physics.add.sprite(400, 300, 'player'); + this.player.setCollideWorldBounds(true); + + // 获取插件实例(更可靠的方式) + this.inputPlugin = this.plugins.get('InputPlugin'); + + if (this.inputPlugin) { + // 添加触摸轮盘 + this.joystickId = this.inputPlugin.createJoystick({ + id: 'movement', + x: 100, + y: this.sys.game.config.height - 100, + radius: 60, + maxDistance: 40 + }); + } else { + console.error('InputPlugin 未找到!'); + } + + // 添加提示文字 + this.add.text(10, 10, '使用 WASD/方向键 或 触摸轮盘移动', { fill: '#ffffff', fontSize: '16px' }); + } + + update() { + // 重置速度 + this.player.setVelocity(0); + + // 键盘输入 + const cursors = this.input.keyboard.createCursorKeys(); + const keys = this.input.keyboard.addKeys('W,A,S,D'); + + if (cursors.up.isDown || keys.W.isDown) { + this.player.setVelocityY(-this.moveSpeed); + } + if (cursors.down.isDown || keys.S.isDown) { + this.player.setVelocityY(this.moveSpeed); + } + if (cursors.left.isDown || keys.A.isDown) { + this.player.setVelocityX(-this.moveSpeed); + } + if (cursors.right.isDown || keys.D.isDown) { + this.player.setVelocityX(this.moveSpeed); + } + + // 触摸轮盘输入(带归一化) + if (this.inputPlugin && this.joystickId) { + const joystickNormalized = this.inputPlugin.getJoystickNormalized(this.joystickId); + if (joystickNormalized.x !== 0 || joystickNormalized.y !== 0) { + this.player.setVelocityX(joystickNormalized.x * this.moveSpeed); + this.player.setVelocityY(joystickNormalized.y * this.moveSpeed); + } + } + } +} diff --git a/src/scenes/JoystickTestScene.js b/src/scenes/JoystickTestScene.js new file mode 100644 index 0000000..57742f5 --- /dev/null +++ b/src/scenes/JoystickTestScene.js @@ -0,0 +1,215 @@ +// scenes/JoystickTestScene.js + +import { InputPlugin } from '../plugins/InputPlugin.js'; + +export class JoystickTestScene extends Phaser.Scene { + constructor() { + super('JoystickTestScene'); + this.moveSpeed = 200; + this.sprintSpeed = 400; // 冲刺速度 + this.inputPlugin = null; + this.joystickId = null; + this.sprintButtonId = null; + this.isSprinting = false; + } + + preload() { + } + + create() { + // 创建背景 + this.cameras.main.setBackgroundColor('#2c3e50'); + + // 创建玩家 + const graphics = this.make.graphics(); + graphics.fillStyle(0x3498db, 1); + graphics.fillRect(0, 0, 40, 40); + graphics.generateTexture('player', 40, 40); + graphics.destroy(); + + this.player = this.physics.add.sprite(400, 300, 'player'); + this.player.setCollideWorldBounds(true); + + // 直接创建 InputPlugin 实例(绕过插件系统) + try { + this.inputPlugin = new InputPlugin(this, this.plugins); + console.log('直接创建 InputPlugin:', this.inputPlugin); + + // 手动启动插件 + this.inputPlugin.boot(); + + // 测试插件方法 + const testResult = this.inputPlugin.testMethod(); + console.log('插件测试结果:', testResult); + + // 添加移动轮盘 + this.joystickId = this.inputPlugin.createJoystick({ + id: 'movement', + x: 150, + y: this.sys.game.config.height - 150, + radius: 80, + maxDistance: 60 + }); + console.log('创建轮盘:', this.joystickId); + + // 添加冲刺按钮 + this.sprintButtonId = this.inputPlugin.createButton({ + id: 'sprint', + x: this.sys.game.config.width - 150, + y: this.sys.game.config.height - 150, + width: 100, + height: 100, + label: '冲刺', + onDown: () => { + this.isSprinting = true; + console.log('开始冲刺'); + }, + onUp: () => { + this.isSprinting = false; + console.log('结束冲刺'); + } + }); + console.log('创建冲刺按钮:', this.sprintButtonId); + + // 添加提示文字 + this.add.text(10, 10, '触摸轮盘测试场景', { + fill: '#ffffff', + fontSize: '24px', + fontFamily: 'Arial' + }); + + this.add.text(10, 50, '使用左侧轮盘移动', { + fill: '#ffffff', + fontSize: '16px', + fontFamily: 'Arial' + }); + + this.add.text(10, 80, '使用右侧按钮冲刺', { + fill: '#ffffff', + fontSize: '16px', + fontFamily: 'Arial' + }); + + this.add.text(10, 110, '使用 WASD 或方向键也可以移动', { + fill: '#ffffff', + fontSize: '16px', + fontFamily: 'Arial' + }); + + // 显示轮盘状态 + this.statusText = this.add.text(10, 150, '轮盘状态: 未激活', { + fill: '#ffffff', + fontSize: '14px', + fontFamily: 'Arial' + }); + + // 添加适配器架构信息 + this.add.text(10, 300, '适配器架构信息:', { + fill: '#ffffff', + fontSize: '16px', + fontFamily: 'Arial' + }); + + this.add.text(10, 330, '- 核心: InputSystem (引擎无关)', { + fill: '#ffffff', + fontSize: '14px', + fontFamily: 'Arial' + }); + + this.add.text(10, 360, '- 适配器: PhaserInputAdapter', { + fill: '#ffffff', + fontSize: '14px', + fontFamily: 'Arial' + }); + + this.add.text(10, 390, '- 插件: InputPlugin (场景插件)', { + fill: '#ffffff', + fontSize: '14px', + fontFamily: 'Arial' + }); + + } catch (error) { + console.error('创建 InputPlugin 失败:', error); + this.add.text(10, 10, 'InputPlugin 初始化失败!', { + fill: '#ff0000', + fontSize: '24px' + }); + this.add.text(10, 50, '错误: ' + error.message, { + fill: '#ffffff', + fontSize: '16px' + }); + } + } + + update() { + // 重置速度 + this.player.setVelocity(0); + + // 确定当前速度(根据是否冲刺) + const currentSpeed = this.isSprinting ? this.sprintSpeed : this.moveSpeed; + + // 键盘输入 + const cursors = this.input.keyboard.createCursorKeys(); + const keys = this.input.keyboard.addKeys('W,A,S,D,SHIFT'); + + let hasKeyboardInput = false; + + // 检查冲刺键(Shift) + if (keys.SHIFT.isDown) { + this.isSprinting = true; + } else if (!this.inputPlugin || this.inputPlugin.isButtonUp(this.sprintButtonId)) { + this.isSprinting = false; + } + + if (cursors.up.isDown || keys.W.isDown) { + this.player.setVelocityY(-currentSpeed); + hasKeyboardInput = true; + } + if (cursors.down.isDown || keys.S.isDown) { + this.player.setVelocityY(currentSpeed); + hasKeyboardInput = true; + } + if (cursors.left.isDown || keys.A.isDown) { + this.player.setVelocityX(-currentSpeed); + hasKeyboardInput = true; + } + if (cursors.right.isDown || keys.D.isDown) { + this.player.setVelocityX(currentSpeed); + hasKeyboardInput = true; + } + + // 触摸轮盘输入 + if (this.inputPlugin && this.joystickId) { + try { + const joystickNormalized = this.inputPlugin.getJoystickNormalized(this.joystickId); + const joystickMagnitude = this.inputPlugin.getJoystickMagnitude(this.joystickId); + + if (joystickNormalized.x !== 0 || joystickNormalized.y !== 0) { + this.player.setVelocityX(joystickNormalized.x * currentSpeed); + this.player.setVelocityY(joystickNormalized.y * currentSpeed); + } + + // 更新状态文字 + if (this.statusText) { + const sprintStatus = this.isSprinting ? '(冲刺中)' : ''; + if (joystickMagnitude > 0) { + this.statusText.setText(`轮盘状态: 激活${sprintStatus}\n力度: ${joystickMagnitude.toFixed(2)}\n方向: X=${joystickNormalized.x.toFixed(2)}, Y=${joystickNormalized.y.toFixed(2)}\n速度: ${currentSpeed}`); + } else if (hasKeyboardInput) { + this.statusText.setText(`轮盘状态: 未激活${sprintStatus} (使用键盘)\n速度: ${currentSpeed}`); + } else { + this.statusText.setText(`轮盘状态: 未激活${sprintStatus}\n速度: ${currentSpeed}`); + } + } + } catch (error) { + console.error('轮盘更新失败:', error); + } + } + } + + destroy() { + if (this.inputPlugin) { + this.inputPlugin.destroy(); + } + super.destroy(); + } +}