feat(SS) 添加地图

This commit is contained in:
chicrobot committed 2026-02-04 21:48:37 +08:00
1 parent 2616dc21f1
commit d3669bd44e
13 files changed
+1358 -98

No files matched your search

+196
View File
@@ -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 对象进行单元测试
- ✅ 维护:每个适配器独立维护,降低复杂度
**何时使用适配器模式:**
- 需要集成第三方库或框架
- 需要支持多种实现方式
- 需要在不修改现有代码的情况下扩展功能
- 需要提高代码的可测试性
+26 -4
View File
@@ -5,17 +5,34 @@
* 当创建的角色超过3个时,点击已存在的角色会为其设置纹理 * 当创建的角色超过3个时,点击已存在的角色会为其设置纹理
* 当设置纹理的角色超过3个时,自动切换到 MenuScene 场景 * 当设置纹理的角色超过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() { constructor(sceneKey = 'BreatheLife') {
super('BreatheLife') super(sceneKey)
this.nextScene = 'MenuScene' 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() { 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(`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`) 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.spriteAnimCount = 0
this.targetCount = 3 // 目标角色数量 this.targetCount = 3 // 目标角色数量
this.createTilemap() // 创建瓦片地图
this.initFrameRateText() // 初始化帧率显示文本
this.initDayNight(this.toGameTime('2000-01-01 12:00:00')) // 初始化时间,白天黑夜状态
// 监听鼠标点击事件,点击后创建一个新的角色 // 监听鼠标点击事件,点击后创建一个新的角色
this.input.on('pointerdown', () => { this.input.on('pointerdown', () => {
// 获取点击位置的坐标 // 获取点击位置的坐标
@@ -84,6 +105,7 @@ export default class BreatheLife extends Phaser.Scene {
}else if (this.spriteCount < 3) { }else if (this.spriteCount < 3) {
// 创建一个新的角色 sprite,缩放为1倍,并设置原点为中心底部 // 创建一个新的角色 sprite,缩放为1倍,并设置原点为中心底部
const sprite = this.physics.add.sprite(x, y, 'player').setScale(1).setOrigin(0.5, 1) const sprite = this.physics.add.sprite(x, y, 'player').setScale(1).setOrigin(0.5, 1)
sprite.setDepth(this.SpriteDepth) // 设置角色层级
// 设置角色碰撞世界边界 // 设置角色碰撞世界边界
sprite.setCollideWorldBounds(true) sprite.setCollideWorldBounds(true)
+73 -89
View File
@@ -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() { constructor(sceneKey = 'WorldLand') {
super('WorldLand') super(sceneKey)
this.nextScene = 'BreatheLife' this.nextScene = 'BreatheLife'
this.timeManager = new GameTimeManager() // 创建游戏时间管理器实例
this.UIDepth = 100 // UI层级深度,确保在其他元素之上 this.UIDepth = 100 // UI层级深度,确保在其他元素之上
this.maskDepth = 99 // 黑夜遮罩层级,低于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() { create() {
// 设置场景背景颜色为深灰色 // 设置场景背景颜色为深灰色
this.cameras.main.setBackgroundColor('#999999') this.cameras.main.setBackgroundColor('#999999')
// 初始化计数器,用于跟踪创建的小草数量
this.smallGrassCount = 0
// 监听鼠标点击事件,点击后执行场景过渡动画 this.initInput() // 初始化输入事件
this.input.on('pointerdown', () => { this.initDayNight(this.toGameTime('2000-01-01 17:57:00')) // 初始化时间,白天黑夜状态
this.scene.start(this.nextScene);
})
// 监听键盘按键事件,按键后执行场景过渡动画 this.createTilemap() // 创建瓦片地图
this.input.keyboard.on('keydown', () => {
this.scene.start(this.nextScene);
})
this.initDayNight(this.toGameTime('1999-12-30 23:57:00')) // 初始化时间,白天黑夜状态
this.initFrameRateText() // 初始化帧率显示文本 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() { update() {
this.showFrameRate() // 显示当前帧率 this.showFrameRate() // 显示当前帧率
this.updateGameTime() // 更新游戏时间 this.updateGameTime() // 更新游戏时间
@@ -49,7 +76,31 @@ export default class WorldLand extends Phaser.Scene {
this.updateDayNight() // 更新昼夜状态 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() { initFrameRateText() {
// 创建帧率显示文本 // 创建帧率显示文本
this.frameRateText = this.add.text( this.frameRateText = this.add.text(
@@ -61,74 +112,7 @@ export default class WorldLand extends Phaser.Scene {
).setScrollFactor(0).setDepth(this.UIDepth) // 固定文本,不随相机移动,UI层级 ).setScrollFactor(0).setDepth(this.UIDepth) // 固定文本,不随相机移动,UI层级
} }
showFrameRate() { showFrameRate() {
// 获取当前帧率 const frameRate = this.game.loop.actualFps // 获取当前帧率
const frameRate = this.game.loop.actualFps this.frameRateText.setText(`Frame Rate: ${frameRate.toFixed(2)}`) // 更新帧率显示文本
// 更新帧率显示文本
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
}
} }
} }
+13 -5
View File
@@ -7,10 +7,11 @@
export default class WorldTime extends Phaser.Scene { export default class WorldTime extends Phaser.Scene {
/** /**
* 构造函数 * 构造函数
* 初始化场景并设置场景键名为 'WorldTime' * 初始化场景并设置场景键名
* @param {string} sceneKey - 场景键名
*/ */
constructor() { constructor(sceneKey = 'WorldTime') {
super('WorldTime') super(sceneKey)
this.nextScene = 'WorldLand' this.nextScene = 'WorldLand'
this.gameTime = 0 // 游戏中的时间戳(单位:秒) this.gameTime = 0 // 游戏中的时间戳(单位:秒)
@@ -34,6 +35,15 @@ export default class WorldTime extends Phaser.Scene {
// 设置场景背景颜色为深灰色 // 设置场景背景颜色为深灰色
this.cameras.main.setBackgroundColor('#999999') this.cameras.main.setBackgroundColor('#999999')
this.initInput() // 初始化输入事件
this.initDayNight(this.toGameTime('1999-12-30 23:57:00')) // 初始化时间,白天黑夜状态
}
/**
* 初始化输入事件
* 监听鼠标点击和键盘按键,触发场景过渡
*/
initInput() {
// 监听鼠标点击事件,点击后执行场景过渡动画 // 监听鼠标点击事件,点击后执行场景过渡动画
this.input.on('pointerdown', () => { this.input.on('pointerdown', () => {
this.scene.start(this.nextScene); this.scene.start(this.nextScene);
@@ -43,8 +53,6 @@ export default class WorldTime extends Phaser.Scene {
this.input.keyboard.on('keydown', () => { this.input.keyboard.on('keydown', () => {
this.scene.start(this.nextScene); this.scene.start(this.nextScene);
}) })
this.initDayNight(this.toGameTime('2000-01-01 17:57:00')) // 初始化时间,白天黑夜状态
} }
/** /**
* 更新游戏时间 * 更新游戏时间
+34
View File
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Phaser</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #000;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
}
canvas {
display: block;
max-width: 100vw;
max-height: 100vh;
}
</style>
</head>
<body>
<script src="../phaser.min.js"></script>
<script type="module" src="main.js"></script>
</body>
</html>
+46
View File
@@ -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);
+113
View File
@@ -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();
}
}
+74
View File
@@ -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();
}
}
+90
View File
@@ -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() 必须由子类实现');
}
}
+288
View File
@@ -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 已销毁');
}
}
+116
View File
@@ -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 已销毁');
}
}
+74
View File
@@ -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);
}
}
}
}
+215
View File
@@ -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();
}
}