From 63f38c59eb31bf214158947f59d944fe5d84d1c9 Mon Sep 17 00:00:00 2001 From: wvellichor Date: Sun, 18 Jan 2026 00:55:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(skill)=20=E6=B7=BB=E5=8A=A0=E6=94=BB?= =?UTF-8?q?=E5=87=BB=E6=8A=80=E8=83=BD=E7=89=B9=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-DETAIL.md | 870 +++++++++++++++++ main.js | 5 +- scenes/BootScene.js | 1 + scenes/testScenes/AttackSkillTestScene.js | 1082 +++++++++++++++++++++ 4 files changed, 1957 insertions(+), 1 deletion(-) create mode 100644 README-DETAIL.md create mode 100644 scenes/testScenes/AttackSkillTestScene.js diff --git a/README-DETAIL.md b/README-DETAIL.md new file mode 100644 index 0000000..90fd708 --- /dev/null +++ b/README-DETAIL.md @@ -0,0 +1,870 @@ +# 场景技术实现方案详细解读 + +## 项目概述 + +本项目是一个基于Phaser.js游戏框架的测试项目,包含多个测试场景,用于验证不同游戏功能的技术实现。项目使用模块化设计,将不同功能封装到独立的系统和组件中,便于测试和扩展。 + +## 核心系统架构 + +### 1. 输入系统 (InputSystem) +- 处理玩家的键盘、鼠标和虚拟摇杆输入 +- 支持多种输入模式切换 +- 提供统一的输入处理接口 + +### 2. 选择系统 (SelectionSystem) +- 处理单位的选择逻辑 +- 支持单选和框选功能 +- 管理选中状态的视觉反馈 + +### 3. 命令系统 (CommandSystem) +- 处理玩家对单位的命令(如移动、攻击等) +- 实现命令的分发和执行 +- 支持不同类型命令的处理 + +### 4. 单位系统 (Unit) +- 游戏中可控制实体的基类 +- 实现基本的移动、碰撞和状态管理 +- 支持目标点移动和路径计算 + +### 5. 物品系统 (ItemSystem) +- 管理游戏中的物品 +- 实现物品的拾取、使用和效果 +- 支持物品的属性和分类 + +## 场景技术实现详解 + +### 1. 攻击技能测试场景 (AttackSkillTestScene) + +#### 功能概述 +测试攻击技能的释放和效果,包括元歌的傀儡控制和扇形飞镖技能。 + +#### 核心技术实现 +- **技能轮盘系统**:实现了基于拖拽的技能释放机制 +- **傀儡控制**:实现了元歌与傀儡的切换控制 +- **技能预览**:为飞镖技能添加了扇形范围预览 +- **飞镖攻击**:实现了多方向扇形飞镖的发射和碰撞检测 + +#### 关键代码分析 +```javascript +// 技能轮盘创建 +createSkillWheel() { + // 技能按钮扇形排列 + const skills = [1, 2, 3, 4]; + const startAngle = -Math.PI; + const endAngle = -Math.PI / 2; + const angleRange = endAngle - startAngle; + const angleStep = angleRange / (skills.length - 1); + + // 为每个技能按钮添加点击事件 + button.on('pointerdown', (pointer) => { + // 选中技能后,将轮盘中心设置为技能按钮的中心位置 + this.skillWheel.centerX = x; + this.skillWheel.centerY = y; + this.skillWheel.base.setPosition(x, y); + this.skillWheel.base.setVisible(true); + + this.skillWheel.activeSkill = skillIndex; + this.startSkillCast(pointer); + }); +} +``` + +#### 技术特点 +- 使用Phaser的图形系统绘制技能预览 +- 实现了基于角度计算的技能方向控制 +- 支持技能释放的视觉反馈 +- 实现了复杂的角色切换控制逻辑 + +### 2. 技能移动效果测试场景 (SkillTestScene) + +#### 功能概述 +测试技能的移动效果,包括元歌的傀儡控制和技能位移。 + +#### 核心技术实现 +- **技能轮盘系统**:与攻击技能场景类似的技能释放机制 +- **傀儡控制**:实现元歌与傀儡的切换和位置交换 +- **技能位移**:实现角色通过技能进行的位置移动 + +#### 关键代码分析 +```javascript +// 元歌技能释放 +castYuangeSkill(skillIndex) { + const yuange = this.characters.find(char => char.name === 'yuange'); + + // 计算技能释放方向 + const pointer = this.input.activePointer; + const directionX = pointer.x - this.skillWheel.centerX; + const directionY = pointer.y - this.skillWheel.centerY; + + if (this.isControllingPuppet) { + // 控制傀儡时的技能 + switch (skillIndex) { + case 1: // 收回傀儡 + this.switchToYuange(); + break; + case 2: // 交换位置 + // 实现位置交换逻辑 + break; + case 4: // 傀儡快速移动并拉本体 + // 实现快速移动逻辑 + break; + } + } else { + // 控制本体时的技能 + switch (skillIndex) { + case 1: // 扔出傀儡 + // 实现傀儡生成和控制切换 + break; + case 4: // 本体向技能指向方向移动 + // 实现本体移动逻辑 + break; + } + } +} +``` + +#### 技术特点 +- 实现了复杂的角色状态管理 +- 支持角色间的位置交换 +- 实现了基于技能方向的位移计算 + +### 3. 种田功能测试场景 (FarmingTestScene) + +#### 功能概述 +测试游戏中的种田系统,包括种子种植、作物生长和收获。 + +#### 核心技术实现 +- **种植模式**:实现了专门的种植模式和交互 +- **作物生长**:实现了作物的生长周期和状态变化 +- **收获系统**:实现了作物的收获和物品掉落 +- **背包集成**:与背包系统集成,支持种子的存储和管理 + +#### 关键代码分析 +```javascript +// 开始种植模式 +startPlantingMode(seed, user) { + this.isPlantingMode = true; + this.selectedSeed = seed; + + this.generalInfoText.setText(`点击地面种植 ${seed.name}`); + + // 显示种植光标 + this.plantingCursor = this.add.text(this.input.mousePointer.x, this.input.mousePointer.y, '🌱', { + fontSize: '24px' + }); + + this.plantingCursor.setOrigin(0.5, 0.5); + this.plantingCursor.setScrollFactor(0, 0); + this.plantingCursor.setDepth(100); + + // 跟随鼠标移动 + const scene = this; + this.mouseMoveListener = (pointer) => { + if (scene.plantingCursor) { + scene.plantingCursor.setPosition(pointer.x, pointer.y); + } + }; + + this.input.on('pointermove', this.mouseMoveListener); +} +``` + +#### 技术特点 +- 实现了专门的种植交互模式 +- 支持作物的生长周期管理 +- 与背包系统的无缝集成 +- 实现了种植光标的跟随效果 + +### 4. 角色动画测试场景 (CharacterAnimationTestScene) + +#### 功能概述 +测试角色的动画效果,包括移动动画和角色切换。 + +#### 核心技术实现 +- **动画系统**:使用Phaser的动画系统实现角色动画 +- **精灵帧管理**:加载和管理多个动画帧 +- **动画控制**:根据角色状态控制动画播放 +- **角色切换**:实现不同角色之间的切换 + +#### 关键代码分析 +```javascript +// 创建角色动画 +createCharacterAnimations() { + console.log('创建角色动画...'); + + // 创建Player0的移动动画 + this.anims.create({ + key: 'player0-move', + frames: [ + { key: 'player0-0' }, + { key: 'player0-1' }, + { key: 'player0-2' }, + { key: 'player0-3' }, + { key: 'player0-4' }, + { key: 'player0-5' }, + { key: 'player0-6' }, + { key: 'player0-7' } + ], + frameRate: 10, + repeat: -1 + }); + + // 创建Player1的移动动画 + this.anims.create({ + key: 'player1-move', + frames: [ + { key: 'player1-0' }, + { key: 'player1-1' }, + { key: 'player1-2' }, + { key: 'player1-3' }, + { key: 'player1-4' }, + { key: 'player1-5' }, + { key: 'player1-6' }, + { key: 'player1-7' } + ], + frameRate: 10, + repeat: -1 + }); +} +``` + +#### 技术特点 +- 使用Phaser的动画系统实现流畅的角色动画 +- 支持多角色动画切换 +- 实现了动画与移动状态的同步 +- 支持角色朝向的自动调整 + +### 5. 地图编辑测试场景 (MapEditTestScene) + +#### 功能概述 +测试地图编辑功能,包括瓦片放置、删除和地图管理。 + +#### 核心技术实现 +- **瓦片选择**:实现了瓦片的预览和选择 +- **地图绘制**:支持瓦片的放置和删除 +- **地图管理**:实现了地图的保存和加载 +- **地形编辑**:支持不同地形类型的编辑 + +#### 关键代码分析 +```javascript +// 放置瓦片 +placeTile(pointer) { + // 将屏幕坐标转换为世界坐标 + const worldPoint = this.cameras.main.getWorldPoint(pointer.x, pointer.y); + + // 计算瓦片坐标 + const tileX = Math.floor(worldPoint.x / this.tileSize); + const tileY = Math.floor(worldPoint.y / this.tileSize); + + // 检查瓦片坐标是否在地图范围内 + if (tileX < 0 || tileX >= this.mapWidth || tileY < 0 || tileY >= this.mapHeight) { + return; + } + + // 检查是否已经绘制过这个位置,避免重复绘制 + if (this.lastDrawnTile && this.lastDrawnTile.x === tileX && this.lastDrawnTile.y === tileY) { + return; + } + + // 地形模式:只设置地形,不修改瓦片 + if (this.isTerrainMode) { + // 更新地图数据中的地形ID + this.mapData[tileY][tileX].terrainId = this.selectedTerrain; + } + // 瓦片模式:放置或删除瓦片 + else if (this.selectedTile === -1) { + this.removeTile(tileX, tileY); + } else { + // 放置瓦片 + const tileKey = this.tileSet[this.selectedTile]; + + // 检查该位置是否已有瓦片 + const existingTile = this.getTileAt(tileX, tileY); + if (existingTile) { + // 删除现有瓦片 + existingTile.destroy(); + } + + // 计算精确的绘制位置 + const drawX = tileX * this.tileSize; + const drawY = tileY * this.tileSize; + + // 创建新瓦片 + const tile = this.add.image(drawX, drawY, tileKey); + tile.setOrigin(0, 0); + tile.setDepth(2); + this.mapLayer.add(tile); + + // 更新地图数据,包含瓦片索引和地形ID + this.mapData[tileY][tileX] = { + tileIndex: this.selectedTile, + terrainId: this.selectedTerrain + }; + } + + // 记录最后绘制的瓦片位置 + this.lastDrawnTile = { x: tileX, y: tileY }; +} +``` + +#### 技术特点 +- 实现了直观的地图编辑界面 +- 支持瓦片的精确放置和删除 +- 实现了地图的保存和加载功能 +- 支持地形类型的编辑和管理 + +### 6. 地图编辑单瓦片图资源测试场景 (MapEditSinglePicTestScene) + +#### 功能概述 +测试使用单张瓦片图资源进行地图编辑的技术实现。 + +#### 核心技术实现 +- **单瓦片图管理**:使用单张瓦片图替代多张独立瓦片 +- **瓦片裁剪**:实现从单张图中裁剪指定区域作为瓦片 +- **缩放控制**:支持地图的缩放和平移 +- **性能优化**:通过单张图加载提高性能 + +#### 关键代码分析 +```javascript +// 加载瓦片资源 +loadTileAssets() { + console.log('开始加载瓦片资源'); + + // 只加载单张瓦片图,替代多张瓦片资源 + const tilemapKey = 'mapedit-tilemap'; + const tilemapPath = 'assets/PNG/Tiles/Tilemap/tilemap_packed.png'; + console.log(`加载瓦片图: ${tilemapKey} -> ${tilemapPath}`); + this.load.image(tilemapKey, tilemapPath); + + // 加载完成回调 + this.load.on('complete', () => { + console.log('所有资源加载完成!'); + this.assetsLoaded = true; + this.loadingText.setText('资源加载完成!'); + + // 设置瓦片图的过滤模式为NEAREST + if (this.textures.exists(tilemapKey)) { + const texture = this.textures.get(tilemapKey); + texture.setFilter(Phaser.Textures.FilterMode.NEAREST); + console.log(`设置纹理 ${tilemapKey} 的过滤模式为NEAREST`); + } + + // 初始化瓦片集合 - 只使用单张瓦片图键名 + this.tileCount = 234; // 根据Tilesheet.txt,总瓦片数为18x13=234 + this.tilemapKey = tilemapKey; + // 存储瓦片的位置信息,用于后续切割 + this.tileImageSize = 16; // 瓦片大小16x16px + this.tileSpacing = 0; // 瓦片间距0px + this.tileColumns = 18; // 18列 + this.tileRows = 13; // 13行 + }); +} +``` + +#### 技术特点 +- 使用单张瓦片图提高加载性能 +- 实现了从单张图中精确裁剪瓦片 +- 支持地图的缩放和平移操作 +- 提供了更高效的瓦片管理方式 + +### 7. 资源包测试场景 (ResourceTestScene) + +#### 功能概述 +测试游戏资源包的加载和管理,包括图像和音效资源。 + +#### 核心技术实现 +- **资源加载**:批量加载不同类型的资源 +- **资源分类**:按类型组织和显示资源 +- **资源预览**:提供资源的可视化预览 +- **音效测试**:测试音效的加载和播放 + +#### 关键代码分析 +```javascript +// 加载所有资源 +loadAssets() { + // 加载PNG资源 + this.load.image('enemies-tilemap', 'assets/PNG/Enemies/Tilemap/tilemap.png'); + this.load.image('enemies-tilemap-packed', 'assets/PNG/Enemies/Tilemap/tilemap_packed.png'); + + this.load.image('interface-tilemap', 'assets/PNG/Interface/Tilemap/tilemap.png'); + this.load.image('interface-tilemap-packed', 'assets/PNG/Interface/Tilemap/tilemap_packed.png'); + + this.load.image('players-tilemap', 'assets/PNG/Players/Tilemap/tilemap.png'); + this.load.image('players-tilemap-packed', 'assets/PNG/Players/Tilemap/tilemap_packed.png'); + + this.load.image('tiles-tilemap', 'assets/PNG/Tiles/Tilemap/tilemap.png'); + this.load.image('tiles-tilemap-packed', 'assets/PNG/Tiles/Tilemap/tilemap_packed.png'); + + this.load.image('weapons-tilemap', 'assets/PNG/Weapons/Tilemap/tilemap.png'); + this.load.image('weapons-tilemap-packed', 'assets/PNG/Weapons/Tilemap/tilemap_packed.png'); + + // 加载前几个敌人瓦片 + for (let i = 0; i < 8; i++) { + this.load.image(`enemy-tile-${i}`, `assets/PNG/Enemies/Tiles/tile_${String(i).padStart(4, '0')}.png`); + } + + // 加载音效资源 + this.load.audio('coin-a', 'assets/Sounds/coin-a.ogg'); + this.load.audio('coin-b', 'assets/Sounds/coin-b.ogg'); + this.load.audio('jump-a', 'assets/Sounds/jump-a.ogg'); + this.load.audio('shoot-a', 'assets/Sounds/shoot-a.ogg'); + this.load.audio('explosion-a', 'assets/Sounds/explosion-a.ogg'); + this.load.audio('hurt-a', 'assets/Sounds/hurt-a.ogg'); +} +``` + +#### 技术特点 +- 实现了批量资源加载和管理 +- 提供了资源的分类和预览 +- 支持音效资源的加载和播放 +- 实现了资源加载进度的可视化反馈 + +### 8. 物品列表菜单测试场景 (ItemListTestScene) + +#### 功能概述 +测试物品列表菜单的显示和交互功能。 + +#### 核心技术实现 +- **物品列表UI**:实现了物品的列表显示 +- **物品筛选**:支持按类型筛选物品 +- **物品交互**:实现了物品的选择和操作 +- **背包集成**:与背包系统集成,支持物品的快速操作 + +#### 关键代码分析 +```javascript +// 初始化物品列表UI +initItemListUI() { + // 创建物品列表UI,位于屏幕左下角 + const x = 10; + const y = this.sys.game.config.height - 350; + this.itemListUI = new ItemListUI(this, x, y); +} + +// 创建测试物品 +createTestItems() { + // 等待物品数据加载完成 + this.itemManager.onDataLoaded(() => { + // 创建初始物品 + this.createInitialItems(); + + // 绑定物品点击事件 + this.events.on('item-clicked', (item) => { + this.showItemInfo(item); + }); + }); +} +``` + +#### 技术特点 +- 实现了专门的物品列表UI +- 支持物品的快速选择和操作 +- 与背包系统的无缝集成 +- 提供了物品的详细信息显示 + +### 9. 背包系统测试场景 (InventoryTestScene) + +#### 功能概述 +测试背包系统的功能,包括物品存储、管理和使用。 + +#### 核心技术实现 +- **背包UI**:实现了网格布局的背包界面 +- **物品管理**:支持物品的添加、移除和移动 +- **物品使用**:实现了物品的使用和效果 +- **容量管理**:实现了背包容量的限制和管理 + +#### 关键代码分析 +```javascript +// 初始化背包系统 +initInventory() { + // 创建背包(6x4网格) + this.player.inventory = new Inventory(6, 4); + + // 创建背包UI(会自动固定在右侧) + this.inventoryUI = new InventoryUI(this, this.player.inventory, 40); +} + +// 显示物品信息 +showItemInfo(item) { + const unit = this.player; + const isInRange = item.isInInteractionRange(unit); + + const info = item.getInfo(); + let infoText = `${info.name}\n`; + infoText += `${info.description}\n`; + infoText += `类型: ${info.type}\n`; + infoText += `重量: ${info.weight}kg\n`; + infoText += `体积: ${info.volume}L\n`; + infoText += `价值: ${info.value}金币\n`; + infoText += `尺寸: ${info.gridSize.width}x${info.gridSize.height}\n`; + infoText += `距离: ${Math.round(Math.sqrt(Math.pow(item.x - unit.x, 2) + Math.pow(item.y - unit.y, 2)))}m\n`; + + // 显示物品属性 + infoText += '属性: '; + const properties = []; + for (const [key, value] of Object.entries(info.properties)) { + properties.push(`${key}: ${value}`); + } + infoText += properties.join(', '); + + this.itemInfoText.setText(infoText); +} +``` + +#### 技术特点 +- 实现了网格布局的背包系统 +- 支持物品的拖拽和管理 +- 实现了物品的详细信息显示 +- 与物品系统的无缝集成 + +### 10. 物品系统测试场景 (ItemTestScene) + +#### 功能概述 +测试物品系统的基本功能,包括物品的创建、属性和交互。 + +#### 核心技术实现 +- **物品创建**:实现了不同类型物品的创建 +- **物品属性**:支持物品的各种属性和效果 +- **物品交互**:实现了物品的拾取和使用 +- **物品分类**:支持不同类型物品的管理 + +#### 关键代码分析 +```javascript +// 创建初始物品 +createInitialItems() { + // 在不同位置创建各种类型的物品 + const itemTypes = ['food', 'tool', 'medical', 'weapon']; + const unit = this.unitGroup.getFirstAlive(); + + for (let i = 0; i < itemTypes.length; i++) { + const x = unit.x + (i - 2) * 200; + const y = unit.y - 200; + this.itemManager.createItemByType(itemTypes[i], x, y); + } +} + +// 显示物品信息 +showItemInfo(item) { + const unit = this.unitGroup.getFirstAlive(); + const isInRange = item.isInInteractionRange(unit); + + const info = item.getInfo(); + let infoText = `${info.name}\n`; + infoText += `${info.description}\n`; + infoText += `类型: ${info.type}\n`; + infoText += `重量: ${info.weight}kg\n`; + infoText += `体积: ${info.volume}L\n`; + infoText += `价值: ${info.value}金币\n`; + infoText += `距离: ${Math.round(Math.sqrt(Math.pow(item.x - unit.x, 2) + Math.pow(item.y - unit.y, 2)))}m\n`; + + // 显示物品属性 + infoText += '属性: '; + const properties = []; + for (const [key, value] of Object.entries(info.properties)) { + properties.push(`${key}: ${value}`); + } + infoText += properties.join(', '); + + this.itemInfoText.setText(infoText); +} +``` + +#### 技术特点 +- 实现了物品的创建和管理 +- 支持物品的属性和效果 +- 实现了物品的交互和使用 +- 提供了物品的详细信息显示 + +### 11. 小地图测试场景 (MiniMapTestScene) + +#### 功能概述 +测试小地图的显示和交互功能。 + +#### 核心技术实现 +- **小地图UI**:实现了游戏世界的缩略图显示 +- **单位显示**:在小地图上显示单位的位置 +- **相机控制**:通过小地图控制主相机视角 +- **交互功能**:支持点击小地图快速移动视角 + +#### 关键代码分析 +```javascript +// 创建小地图 +this.miniMap = new MiniMap(this, { + worldSize: worldSize, + mapSize: 200, + x: this.sys.game.config.width - 210, + y: 10 +}); + +// 添加场景说明文本 +const titleText = this.add.text(10, 10, '测试场景6:小地图测试', { + fontSize: '20px', + fill: '#ffffff' +}); +titleText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 +titleText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + +const miniMapText = this.add.text(10, 40, '使用鼠标拖拽小地图或点击小地图快速移动视角', { + fontSize: '16px', + fill: '#ffffff' +}); +``` + +#### 技术特点 +- 实现了游戏世界的缩略图显示 +- 支持通过小地图控制视角 +- 显示单位在小地图上的位置 +- 提供了直观的小地图交互方式 + +### 12. 移动更新逻辑测试场景 (AutoMovementTestScene) + +#### 功能概述 +测试单位的自动移动逻辑和路径计算。 + +#### 核心技术实现 +- **自动移动**:实现单位的自动路径寻找和移动 +- **目标点设置**:支持为单位设置移动目标 +- **路径计算**:实现基本的路径计算和避障 +- **碰撞检测**:实现单位间的碰撞避免 + +#### 关键代码分析 +```javascript +// 创建多个测试单位,分布在不同位置 +const unitPositions = [ + { x: -300, y: -300 }, + { x: 300, y: -300 }, + { x: -300, y: 300 }, + { x: 300, y: 300 } +]; + +// 自动选中第一个单位 +let firstUnit = null; + +unitPositions.forEach(pos => { + const unit = new Unit(this, pos.x, pos.y, 'player'); + // 为每个单位设置不同的目标点,观察它们的移动 + const targetX = Math.random() * 400 - 200; + const targetY = Math.random() * 400 - 200; + unit.setTarget(targetX, targetY); + this.unitGroup.add(unit); + + // 保存第一个单位的引用 + if (!firstUnit) { + firstUnit = unit; + } +}); +``` + +#### 技术特点 +- 实现了单位的自动路径寻找 +- 支持目标点的设置和更新 +- 实现了基本的碰撞避免 +- 提供了多单位移动的测试环境 + +### 13. 虚拟摇杆控制测试场景 (JoystickMovementTestScene) + +#### 功能概述 +测试虚拟摇杆的控制功能,模拟移动设备上的操作。 + +#### 核心技术实现 +- **虚拟摇杆UI**:实现了屏幕上的虚拟摇杆 +- **输入处理**:将摇杆输入转换为单位移动 +- **灵敏度调整**:支持摇杆灵敏度的调整 +- **移动设备适配**:为移动设备提供操作支持 + +#### 关键代码分析 +```javascript +// 初始化系统 - 只开启虚拟摇杆移动,禁用框选 +this.inputSystem = new InputSystem(this, { + keyboard: false, + mouse: false, + joystick: true, + selection: false +}); +this.selectionSystem = new SelectionSystem(this); +this.commandSystem = new CommandSystem(this); +this.virtualJoystick = new VirtualJoystick(this, true); + +// 添加场景说明文本 +const moveText = this.add.text(10, 40, '使用屏幕右下角的虚拟摇杆控制单位移动', { + fontSize: '16px', + fill: '#ffffff' +}); +``` + +#### 技术特点 +- 实现了屏幕上的虚拟摇杆 +- 支持摇杆输入到移动的转换 +- 为移动设备提供操作支持 +- 实现了摇杆的视觉反馈 + +### 14. 单位选择与移动命令测试场景 (SelectionAndMovementTestScene) + +#### 功能概述 +测试单位的选择和移动命令功能。 + +#### 核心技术实现 +- **单位选择**:实现了单位的单选和框选 +- **移动命令**:实现了向选中单位下达移动命令 +- **命令队列**:支持命令的队列和执行 +- **选中反馈**:实现了选中状态的视觉反馈 + +#### 关键代码分析 +```javascript +// 初始化系统 - 开启鼠标选择、移动和框选功能 +this.inputSystem = new InputSystem(this, { + keyboard: false, + mouse: true, + joystick: false, + selection: true +}); +this.selectionSystem = new SelectionSystem(this); +this.commandSystem = new CommandSystem(this); + +// 创建多个测试单位 +const unitCount = 5; +let firstUnit = null; +for (let i = 0; i < unitCount; i++) { + const x = -200 + i * 100; + const y = 0; + const unit = new Unit(this, x, y, 'player'); + this.unitGroup.add(unit); + if (i === 0) { + firstUnit = unit; + } +} + +// 自动选中第一个单位 +if (firstUnit) { + this.events.emit('select-unit', firstUnit); +} +``` + +#### 技术特点 +- 实现了单位的单选和框选 +- 支持向选中单位下达命令 +- 实现了命令的队列和执行 +- 提供了选中状态的视觉反馈 + +### 15. 目标点移动测试场景 (TargetMovementTestScene) + +#### 功能概述 +测试单位向指定目标点移动的功能。 + +#### 核心技术实现 +- **目标点设置**:实现了通过鼠标设置移动目标点 +- **路径计算**:实现了基本的路径计算 +- **移动平滑**:实现了单位移动的平滑过渡 +- **到达检测**:实现了目标点到达的检测 + +#### 关键代码分析 +```javascript +// 初始化系统 - 只开启鼠标移动,禁用框选 +this.inputSystem = new InputSystem(this, { + keyboard: false, + mouse: true, + joystick: false, + selection: false +}); +this.selectionSystem = new SelectionSystem(this); +this.commandSystem = new CommandSystem(this); + +// 创建一个测试单位 +const unit = new Unit(this, 0, 0, 'player'); +this.unitGroup.add(unit); + +// 自动选中单位 +this.events.emit('select-unit', unit); +``` + +#### 技术特点 +- 实现了通过鼠标设置移动目标 +- 支持单位的平滑移动 +- 实现了目标点到达的检测 +- 提供了直观的目标点设置方式 + +### 16. 相对移动(键盘控制)测试场景 (KeyboardMovementTestScene) + +#### 功能概述 +测试通过键盘控制单位进行相对移动的功能。 + +#### 核心技术实现 +- **键盘输入**:处理键盘方向键的输入 +- **相对移动**:实现单位的相对方向移动 +- **移动速度**:控制单位的移动速度 +- **输入响应**:实现键盘输入的实时响应 + +#### 关键代码分析 +```javascript +// 初始化系统 - 只开启键盘移动,禁用框选 +this.inputSystem = new InputSystem(this, { + keyboard: true, + mouse: false, + joystick: false, + selection: false +}); +this.selectionSystem = new SelectionSystem(this); +this.commandSystem = new CommandSystem(this); + +// 创建一个测试单位 +const unit = new Unit(this, 0, 0, 'player'); +this.unitGroup.add(unit); + +// 自动选中单位 +this.events.emit('select-unit', unit); +``` + +#### 技术特点 +- 实现了通过键盘控制单位移动 +- 支持相对方向的移动控制 +- 提供了实时的输入响应 +- 实现了移动速度的控制 + +## 技术架构总结 + +### 1. 模块化设计 +- **系统分离**:将不同功能分离到独立的系统中 +- **组件化**:使用组件化设计,便于复用和扩展 +- **事件驱动**:使用事件系统实现模块间通信 + +### 2. 性能优化 +- **资源管理**:合理管理游戏资源,避免内存泄漏 +- **渲染优化**:使用Phaser的渲染优化功能 +- **计算优化**:优化路径计算等复杂操作 + +### 3. 用户体验 +- **直观交互**:提供直观的用户交互方式 +- **视觉反馈**:为用户操作提供及时的视觉反馈 +- **响应速度**:确保系统对用户输入的快速响应 + +### 4. 可扩展性 +- **模块化设计**:便于添加新功能和系统 +- **配置化**:使用配置文件管理游戏参数 +- **抽象接口**:提供抽象接口,便于扩展 + +## 技术亮点 + +1. **多输入模式支持**:实现了键盘、鼠标和虚拟摇杆的统一输入处理 +2. **模块化系统架构**:使用模块化设计,便于测试和扩展 +3. **流畅的动画系统**:实现了角色的流畅动画效果 +4. **直观的地图编辑**:提供了直观的地图编辑工具 +5. **完整的物品系统**:实现了物品的完整生命周期管理 +6. **智能的移动系统**:实现了单位的智能移动和避障 +7. **响应式UI设计**:UI元素会根据屏幕大小自动调整 +8. **资源管理优化**:通过单张瓦片图等方式优化资源加载 + +## 未来扩展方向 + +1. **网络多人游戏**:添加网络同步功能,支持多人游戏 +2. **高级AI系统**:实现更复杂的AI行为和决策 +3. **物理引擎集成**:更深入地集成物理引擎,实现更真实的物理效果 +4. **音效系统**:完善音效系统,添加更多音频反馈 +5. **剧情系统**:添加剧情管理和任务系统 +6. **多人协作**:实现玩家间的协作机制 +7. **自定义角色**:添加角色自定义和升级系统 +8. **世界生成**:实现 procedural 世界生成 + +## 结论 + +本项目通过多个测试场景全面验证了游戏开发中的核心技术实现,包括输入处理、单位控制、物品系统、地图编辑等。采用模块化设计和Phaser.js框架,构建了一个可扩展的游戏开发基础架构。每个测试场景都针对特定功能进行了详细测试,为游戏开发提供了有价值的技术参考。 + +项目的技术实现注重代码质量和可扩展性,使用了现代JavaScript的最佳实践,如模块化设计、事件驱动架构和组件化开发。这些技术方案不仅适用于当前的测试项目,也可以作为实际游戏开发的基础架构参考。 \ No newline at end of file diff --git a/main.js b/main.js index 2bae203..dc6c167 100644 --- a/main.js +++ b/main.js @@ -30,6 +30,8 @@ import MapEditTestScene from './scenes/testScenes/MapEditTestScene.js'; import FarmingTestScene from './scenes/testScenes/FarmingTestScene.js'; // 导入技能测试场景 import SkillTestScene from './scenes/testScenes/SkillTestScene.js'; +// 导入攻击技能测试场景 +import AttackSkillTestScene from './scenes/testScenes/AttackSkillTestScene.js'; // 创建游戏的最终配置对象 const config = { @@ -54,7 +56,8 @@ const config = { CharacterAnimationTestScene, MapEditTestScene, FarmingTestScene, - SkillTestScene + SkillTestScene, + AttackSkillTestScene ] }; diff --git a/scenes/BootScene.js b/scenes/BootScene.js index e381e8c..a0a0d4c 100644 --- a/scenes/BootScene.js +++ b/scenes/BootScene.js @@ -20,6 +20,7 @@ export default class BootScene extends Phaser.Scene { // 测试场景列表 const testScenes = [ + { key: 'AttackSkillTestScene', name: '测试场景16:攻击技能测试' }, { key: 'SkillTestScene', name: '测试场景15:技能移动效果测试' }, { key: 'FarmingTestScene', name: '测试场景14:种田功能测试' }, { key: 'CharacterAnimationTestScene', name: '测试场景13:角色动画测试' }, diff --git a/scenes/testScenes/AttackSkillTestScene.js b/scenes/testScenes/AttackSkillTestScene.js new file mode 100644 index 0000000..54fbaa8 --- /dev/null +++ b/scenes/testScenes/AttackSkillTestScene.js @@ -0,0 +1,1082 @@ +// 游戏场景文件:这是游戏的主要场景,包含了游戏的核心逻辑和玩法实现 + +// 导入单位类:用于创建游戏中的角色/单位 +import Unit from '../../objects/Unit.js'; + +// 导入输入系统:处理玩家的键盘和鼠标输入 +import { InputSystem } from '../../systems/InputSystem.js'; + +// 导入选择系统:处理单位的选择逻辑 +import { SelectionSystem } from '../../systems/SelectionSystem.js'; + +// 导入命令系统:处理玩家对单位的命令(如移动、攻击等) +import { CommandSystem } from '../../systems/CommandSystem.js'; + +// 导入虚拟摇杆:用于移动设备上的操作控制 +import { VirtualJoystick } from '../../ui/VirtualJoystick.js'; + +// 导入小地图:显示游戏世界的缩略图 +import { MiniMap } from '../../ui/MiniMap.js'; + +// 定义SkillTestScene类,继承自Phaser.Scene(Phaser框架的场景类) +export default class AttackSkillTestScene extends Phaser.Scene { + // 构造函数:初始化场景,设置场景的唯一键值 + constructor() { + super({ key: 'AttackSkillTestScene' }); + + // 角色相关变量 + this.characters = []; + this.currentlyControlled = null; // 当前控制的角色 + this.isControllingPuppet = false; // 是否正在控制傀儡 + this.puppet = null; // 元歌的傀儡 + + // 技能相关变量 + this.skills = {}; + // 技能预览相关变量 + this.skillPreview = null; + } + + // create方法:当场景创建时自动调用,用于初始化游戏元素 + create() { + // === 0. 添加窗口大小变化监听 === + // 监听浏览器窗口大小变化事件,当窗口大小改变时调用resizeGame方法 + window.addEventListener('resize', this.resizeGame.bind(this)); + + // === 1. 创建玩家纹理=== + // 检查是否已经存在名为'player'的纹理,如果不存在则创建 + if (!this.textures.exists('player')) { + // 创建一个32x32像素的HTML5 Canvas元素 + const canvas = document.createElement('canvas'); + canvas.width = 32; // 设置Canvas宽度 + canvas.height = 32; // 设置Canvas高度 + + // 获取Canvas的2D绘图上下文 + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#000000ff'; // 设置填充颜色为完全不透明的黑色 + ctx.beginPath(); // 开始绘制路径 + // 绘制一个圆形:圆心(16,16),半径14,从0到2π(完整圆形) + ctx.arc(16, 16, 14, 0, Math.PI * 2); + ctx.fill(); // 填充圆形 + + // 将创建的Canvas添加到Phaser的纹理管理器中,命名为'player' + this.textures.addCanvas('player', canvas); + } + + // 创建飞镖纹理 + if (!this.textures.exists('dart')) { + const canvas = document.createElement('canvas'); + canvas.width = 16; + canvas.height = 16; + + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#ff6600'; + ctx.beginPath(); + ctx.moveTo(8, 0); + ctx.lineTo(16, 8); + ctx.lineTo(8, 16); + ctx.lineTo(0, 8); + ctx.closePath(); + ctx.fill(); + + this.textures.addCanvas('dart', canvas); + } + + // === 2. 物理世界和相机边界=== + // 定义游戏世界的大小 + const worldSize = 2000; + + // 设置物理世界的边界:从(-worldSize,-worldSize)到(worldSize*2, worldSize*2) + // 这样游戏世界的中心就是(0,0),范围是从-2000到2000,总共4000x4000大小 + this.physics.world.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + + // 设置相机的边界,与物理世界边界相同 + this.cameras.main.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + + // 设置相机起始位置在地图中心 + // 使用centerOn方法将相机中心定位在地图坐标(0, 0) + this.cameras.main.centerOn(0, 0); // 地图中心坐标是(0, 0) + + // 添加地图网格 + this.createGrid(); + + // === 3. 初始化系统 === + // 创建输入系统实例,禁用鼠标点击移动和框选功能 + this.inputSystem = new InputSystem(this, { + mouse: false, // 禁用鼠标点击移动 + selection: false // 禁用框选功能 + }); + // 创建选择系统实例 + this.selectionSystem = new SelectionSystem(this); + // 创建命令系统实例 + this.commandSystem = new CommandSystem(this); + + // 禁用点击移动和框选功能 + // 阻止选择单位事件触发移动 + this.events.on('select-unit', (unit) => { + // 只允许单位选择,不执行任何移动操作 + console.log('单位已选中:', unit.name); + }); + + // 阻止移动到指定点事件 + this.events.on('move-to-point', (x, y) => { + // 不执行任何移动操作 + return false; + }); + + // 阻止选择开始事件 + this.events.on('selection-start', (pos) => { + // 不执行框选操作 + return false; + }); + + // 阻止选择更新事件 + this.events.on('selection-update', (rect) => { + // 不更新框选区域 + return false; + }); + + // 阻止选择结束事件 + this.events.on('selection-end', (rect) => { + // 不执行框选结束操作 + return false; + }); + // 创建虚拟摇杆实例 + this.virtualJoystick = new VirtualJoystick(this); + + // === 4. 创建角色 === + // 创建一个单位组,用于管理游戏中的所有角色 + this.unitGroup = this.add.group(); + + // 创建元歌 + const yuange = new Unit(this, -500, 0, 'player'); + yuange.name = 'yuange'; + yuange.type = 'yuange'; + this.unitGroup.add(yuange); + this.characters.push(yuange); + + // 创建貂蝉 + const diaochan = new Unit(this, 500, 0, 'player'); + diaochan.name = 'diaochan'; + diaochan.type = 'diaochan'; + this.unitGroup.add(diaochan); + this.characters.push(diaochan); + + // 默认控制元歌 + this.currentlyControlled = yuange; + yuange.setSelected(true); + + // 将默认角色添加到ControlMode的选中列表中,以便虚拟摇杆可以控制 + import('../../systems/ControlMode.js').then(mod => { + mod.clearSelection(); + mod.addToSelection(yuange); + mod.setActiveUnit(yuange); + // 默认锁定相机到元歌 + mod.setCameraLocked(true); + }); + + // === 5. 添加角色之间的碰撞检测 === + // 为单位组添加碰撞检测,使得角色之间可以相互碰撞 + this.physics.add.collider(this.unitGroup, this.unitGroup); + + // === 6. 创建技能轮盘 === + // 创建技能轮盘实例 + this.skillWheel = { + isActive: false, + base: null, + knob: null, + pointer: null, + centerX: 0, + centerY: 0, + radius: 50, + enabled: true, + activeSkill: null, + skillButtons: [], + skillInfo: { + 1: { key: 'Q', color: 0x00ff00 }, + 2: { key: 'W', color: 0x00ffff }, + 3: { key: 'E', color: 0xff00ff }, + 4: { key: 'R', color: 0xffff00 } + } + }; + + // 创建技能轮盘UI + this.createSkillWheel(); + // 绑定技能轮盘事件 + this.bindSkillWheelEvents(); + + // === 7. 添加角色切换按键 === + const kb = this.input.keyboard; + this.switchKey = kb.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); + kb.on('keydown-SPACE', () => this.switchCharacter()); + + // === 7. 添加技能UI提示 === + this.createSkillUI(); + + // === 6. 创建小地图 === + // 创建小地图实例,设置其位置和大小 + this.miniMap = new MiniMap(this, { + worldSize: worldSize, // 游戏世界大小 + mapSize: 200, // 小地图显示大小 + x: this.sys.game.config.width - 200 - 10, // 小地图在屏幕上的x坐标 + y: 10 // 小地图在屏幕上的y坐标 + }); + } + + // 创建地图网格 + createGrid() { + // 创建一个图形对象用于绘制网格 + const graphics = this.add.graphics(); + + // 设置网格线样式 + graphics.lineStyle(1, 0x555555, 0.3); + + // 定义网格参数 + const gridSize = 50; // 网格大小 + const worldSize = 2000; // 世界大小,与物理世界边界一致 + + // 绘制垂直线 + for (let x = -worldSize; x <= worldSize; x += gridSize) { + graphics.moveTo(x, -worldSize); + graphics.lineTo(x, worldSize); + } + + // 绘制水平线 + for (let y = -worldSize; y <= worldSize; y += gridSize) { + graphics.moveTo(-worldSize, y); + graphics.lineTo(worldSize, y); + } + + // 绘制完成 + graphics.strokePath(); + + // 确保网格在所有游戏对象之下 + graphics.setDepth(-10); + + // 让网格随相机移动 + graphics.setScrollFactor(1); + } + + // update方法:每帧自动调用(约60次/秒),用于更新游戏状态 + update() { + // 更新输入系统,处理玩家的键盘和鼠标输入 + this.inputSystem.update(); + + // 更新所有单位的移动逻辑和选中状态 + // 获取单位组中的所有子元素(即所有单位) + this.unitGroup.getChildren().forEach(unit => { + // 如果单位有自己的update方法,则调用它 + if (unit.update) { + unit.update(); + } + // 调用单位的updateMovement方法,更新单位的移动状态 + unit.updateMovement(); + }); + + // 更新小地图显示 + if (this.miniMap) { + this.miniMap.update(); + } + + // 更新虚拟摇杆状态 + if (this.virtualJoystick) { + this.virtualJoystick.update(); + } + + // 相机锁定逻辑:如果有锁定状态且有当前控制的角色,则让相机跟随角色 + import('../systems/ControlMode.js').then(mod => { + if (mod.cameraLocked && this.currentlyControlled) { + this.cameras.main.startFollow(this.currentlyControlled); + } else { + this.cameras.main.stopFollow(); + } + }); + } + + /** + * 创建技能UI提示 + */ + createSkillUI() { + // 创建技能提示文本 + this.skillInfoText = this.add.text(10, this.sys.game.config.height - 100, + '技能施法: 点击右侧技能轮盘拖放\n按空格键切换角色', + { fontSize: '16px', fill: '#ffffff', fontFamily: 'Arial' }) + .setScrollFactor(0) // 固定UI,不随滚动移动 + .setDepth(100); + + // 创建当前控制角色提示 + this.controlInfoText = this.add.text(10, this.sys.game.config.height - 60, + `当前控制: ${this.currentlyControlled.name === 'yuange' ? '元歌' : '貂蝉'}`, + { fontSize: '18px', fill: '#00ff00', fontFamily: 'Arial' }) + .setScrollFactor(0) + .setDepth(100); + } + + /** + * 创建技能轮盘 + */ + createSkillWheel() { + // 技能轮盘基础位置(右下角) + this.skillWheel.centerX = this.scale.width - 100; + this.skillWheel.centerY = this.scale.height - 100; + + // 创建技能轮盘基础(默认隐藏,选中技能时显示) + this.skillWheel.base = this.add.circle(this.skillWheel.centerX, this.skillWheel.centerY, this.skillWheel.radius, 0x444444, 0.5); + this.skillWheel.base.setScrollFactor(0); // 不随相机移动 + this.skillWheel.base.setDepth(100); + this.skillWheel.base.setVisible(false); + + // 创建技能按钮(扇形排列:技能1-4在右下角左上方,大半径扇形分布) + const skills = [1, 2, 3, 4]; + const startAngle = -Math.PI; // 起始角度(-90度,左) + const endAngle = -Math.PI / 2; // 结束角度(0度,上) + const angleRange = endAngle - startAngle; + const angleStep = angleRange / (skills.length - 1); + + this.skillWheel.skillButtons = []; + for (let i = 0; i < skills.length; i++) { + const skillIndex = skills[i]; + // 计算技能按钮的角度(扇形排列) + const angle = startAngle + i * angleStep; + const distance = 120; // 增大技能按钮距离中心的距离 + const x = this.skillWheel.centerX + Math.cos(angle) * distance; + const y = this.skillWheel.centerY + Math.sin(angle) * distance; + + // 创建技能按钮 + const button = this.add.circle(x, y, 25, this.skillWheel.skillInfo[skillIndex].color, 0.6); + button.setScrollFactor(0); + button.setDepth(100); + + // 添加技能按键文本(使用数字1-4代替Q-W-E-R) + const text = this.add.text(x, y, skillIndex.toString(), { + fontSize: '14px', + fill: '#000000', + fontFamily: 'Arial', + fontWeight: 'bold' + }) + .setOrigin(0.5, 0.5) + .setScrollFactor(0) + .setDepth(101); + + // 保存技能信息 + button.skillIndex = skillIndex; + button.skillKey = this.skillWheel.skillInfo[skillIndex].key; + button.x = x; + button.y = y; + + // 添加点击事件 + button.setInteractive({ useHandCursor: true }); + button.on('pointerdown', (pointer) => { + // 选中技能后,将轮盘中心设置为技能按钮的中心位置 + this.skillWheel.centerX = x; + this.skillWheel.centerY = y; + this.skillWheel.base.setPosition(x, y); + this.skillWheel.base.setVisible(true); + + this.skillWheel.activeSkill = skillIndex; + this.startSkillCast(pointer); + }); + + this.skillWheel.skillButtons.push(button); + } + + // 创建轮盘旋钮 + this.skillWheel.knob = this.add.circle(this.skillWheel.centerX, this.skillWheel.centerY, this.skillWheel.radius * 0.5, 0xff0000, 0.8); + this.skillWheel.knob.setScrollFactor(0); + this.skillWheel.knob.setDepth(101); + this.skillWheel.knob.setVisible(false); + } + + /** + * 绑定技能轮盘事件 + */ + bindSkillWheelEvents() { + // 鼠标移动事件 + this.input.on('pointermove', (ptr) => { + // 只有在技能轮盘激活时才处理 + if (!this.skillWheel.isActive || this.skillWheel.pointer !== ptr) { + return; + } + + // 更新旋钮位置(与移动轮盘相同的逻辑) + const dx = ptr.x - this.skillWheel.centerX; + const dy = ptr.y - this.skillWheel.centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance > this.skillWheel.radius) { + // 限制在圆内 + const angle = Math.atan2(dy, dx); + const knobX = this.skillWheel.centerX + Math.cos(angle) * this.skillWheel.radius; + const knobY = this.skillWheel.centerY + Math.sin(angle) * this.skillWheel.radius; + this.skillWheel.knob.setPosition(knobX, knobY); + } else { + this.skillWheel.knob.setPosition(ptr.x, ptr.y); + } + + // 技能释放预览:当技能轮盘激活时,显示技能释放预览 + this.updateSkillPreview(); + }); + + // 鼠标释放事件 + this.input.on('pointerup', (ptr) => { + // 只有在技能轮盘激活时才处理 + if (!this.skillWheel.isActive || this.skillWheel.pointer !== ptr) { + return; + } + + // 完成技能释放 + this.finishSkillCast(); + + // 清除技能预览 + this.clearSkillPreview(); + }); + } + + /** + * 开始技能施法 + */ + startSkillCast(pointer) { + // 设置技能轮盘状态 + this.skillWheel.isActive = true; + this.skillWheel.pointer = pointer; + + // 显示旋钮和轮盘基础 + this.skillWheel.knob.setVisible(true); + this.skillWheel.base.setVisible(true); + + // 设置旋钮初始位置为轮盘中心 + this.skillWheel.knob.setPosition(this.skillWheel.centerX, this.skillWheel.centerY); + + // 根据技能索引改变旋钮颜色 + if (this.skillWheel.activeSkill && this.skillWheel.skillInfo[this.skillWheel.activeSkill]) { + this.skillWheel.knob.fillColor = this.skillWheel.skillInfo[this.skillWheel.activeSkill].color; + } + + // 初始化技能预览 + this.updateSkillPreview(); + } + + /** + * 完成技能施法 + */ + finishSkillCast() { + // 检查是否有激活的技能 + if (!this.skillWheel.activeSkill) { + return; + } + + // 释放技能 + this.castSkill(this.skillWheel.activeSkill); + + // 重置技能轮盘状态 + this.skillWheel.isActive = false; + this.skillWheel.pointer = null; + this.skillWheel.activeSkill = null; + + // 隐藏旋钮和轮盘基础 + this.skillWheel.knob.setVisible(false); + this.skillWheel.base.setVisible(false); + + // 将旋钮和基础重置到默认位置(技能按钮扇形区域的中心) + this.skillWheel.centerX = this.scale.width - 200; + this.skillWheel.centerY = this.scale.height - 200; + this.skillWheel.base.setPosition(this.skillWheel.centerX, this.skillWheel.centerY); + this.skillWheel.knob.setPosition(this.skillWheel.centerX, this.skillWheel.centerY); + } + + /** + * 更新技能轮盘显示 + */ + updateSkillWheelDisplay() { + // 根据当前控制的角色设置技能信息 + if (this.isControllingPuppet) { + // 傀儡的技能设置 + this.skillWheel.skillInfo = { + 1: { key: 'Q', color: 0xff0000 }, // 收回傀儡 + 2: { key: 'W', color: 0x00ffff }, // 交换位置 + 3: { key: 'E', color: 0x0000ff }, // 占位 + 4: { key: 'R', color: 0xffff00 } // 快速移动 + }; + } else if (this.currentlyControlled && this.currentlyControlled.type === 'yuange') { + // 元歌本体的技能设置 + this.skillWheel.skillInfo = { + 1: { key: 'Q', color: 0x00ff00 }, // 扔出傀儡 + 2: { key: 'W', color: 0x00ffff }, // 二技能:飞镖攻击 + 3: { key: 'E', color: 0xff00ff }, // 占位 + 4: { key: 'R', color: 0xffff00 } // 本体移动 + }; + } else if (this.currentlyControlled && this.currentlyControlled.type === 'diaochan') { + // 貂蝉的技能设置 + this.skillWheel.skillInfo = { + 1: { key: 'Q', color: 0x00ff00 }, // 占位 + 2: { key: 'W', color: 0x00ffff }, // 移动 + 3: { key: 'E', color: 0xff00ff }, // 占位 + 4: { key: 'R', color: 0xffff00 } // 占位 + }; + } + + // 更新现有技能按钮的颜色,不重新创建 + this.skillWheel.skillButtons.forEach(button => { + const skillIndex = button.skillIndex; + if (this.skillWheel.skillInfo[skillIndex]) { + button.fillColor = this.skillWheel.skillInfo[skillIndex].color; + } + }); + } + + /** + * 切换控制的角色 + */ + switchCharacter() { + if (this.isControllingPuppet) { + // 如果正在控制傀儡,先切换回本体 + this.switchToYuange(); + } else { + // 否则切换到另一个角色 + const currentIndex = this.characters.indexOf(this.currentlyControlled); + const nextIndex = (currentIndex + 1) % this.characters.length; + + // 取消当前角色的选中状态 + this.currentlyControlled.setSelected(false); + + // 切换到下一个角色 + this.currentlyControlled = this.characters[nextIndex]; + this.currentlyControlled.setSelected(true); + + // 更新UI提示 + this.controlInfoText.setText(`当前控制: ${this.currentlyControlled.name === 'yuange' ? '元歌' : '貂蝉'}`); + + // 更新技能轮盘显示 + this.updateSkillWheelDisplay(); + + // 更新ControlMode的选中列表 + import('../../systems/ControlMode.js').then(mod => { + mod.clearSelection(); + mod.addToSelection(this.currentlyControlled); + mod.setActiveUnit(this.currentlyControlled); + // 自动锁定相机到新角色 + mod.setCameraLocked(true); + }); + } + } + + /** + * 释放技能 + * @param {number} skillIndex 技能索引(1-4) + */ + castSkill(skillIndex) { + // 检查是否有当前控制的角色 + if (!this.currentlyControlled) return; + + // 根据当前控制角色类型和技能索引释放对应技能 + if (this.currentlyControlled.type === 'yuange' || this.currentlyControlled.type === 'yuange-puppet') { + this.castYuangeSkill(skillIndex); + } else if (this.currentlyControlled.type === 'diaochan') { + this.castDiaochanSkill(skillIndex); + } + } + + /** + * 释放元歌技能 + * @param {number} skillIndex 技能索引(1-4) + */ + castYuangeSkill(skillIndex) { + const yuange = this.characters.find(char => char.name === 'yuange'); + + // 计算技能释放方向(基于技能轮盘中心到指针的相对方向) + const pointer = this.input.activePointer; + // 计算轮盘中心到指针的方向向量 + const directionX = pointer.x - this.skillWheel.centerX; + const directionY = pointer.y - this.skillWheel.centerY; + + if (this.isControllingPuppet) { + // 控制傀儡时的技能 + switch (skillIndex) { + case 1: // 技能1:收回傀儡 + this.switchToYuange(); + break; + case 2: // 技能2:本体拉近并交换位置 + if (this.puppet) { + // 记录傀儡当前位置 + const puppetX = this.puppet.x; + const puppetY = this.puppet.y; + + // 获取本体拉近到傀儡一定范围内的坐标 + const closeX = this.puppet.x - 50 * Math.sign(this.puppet.x - yuange.x); + const closeY = this.puppet.y - 50 * Math.sign(this.puppet.y - yuange.y); + + // 傀儡移动到本体拉近后的位置 + this.puppet.setPosition(closeX, closeY); + this.puppet.setVelocity(0, 0); + this.puppet.clearTarget(); + + // 直接设置本体位置到傀儡原本位置 + yuange.setPosition(puppetX, puppetY); + yuange.setVelocity(0, 0); + + } + break; + case 4: // 技能4:傀儡快速移动并拉本体 + if (this.puppet && this.isControllingPuppet) { + // 使用轮盘相对方向计算目标位置 + const maxDistance = 300; + const distance = Math.sqrt(directionX * directionX + directionY * directionY); + + // 计算归一化方向向量 + let normalizedX = 0; + let normalizedY = 0; + if (distance > 0) { + normalizedX = directionX / distance; + normalizedY = directionY / distance; + } + + // 计算目标位置:傀儡当前位置 + 归一化方向向量 * 最大距离 + const targetX = this.puppet.x + normalizedX * maxDistance; + const targetY = this.puppet.y + normalizedY * maxDistance; + + // 傀儡移动到目标位置 + // this.puppet.setTarget(targetX, targetY); + this.puppet.setPosition(targetX, targetY); + this.puppet.setVelocity(0, 0); + this.puppet.clearTarget(); + + // 本体瞬间移动到傀儡附近 + yuange.x = this.puppet.x - 50 * Math.sign(this.puppet.x - yuange.x); + yuange.y = this.puppet.y - 50 * Math.sign(this.puppet.y - yuange.y); + } + break; + } + } else { + // 控制本体时的技能 + switch (skillIndex) { + case 1: // 技能1:扔出傀儡 + if (!this.puppet) { + // 使用轮盘相对方向计算目标位置 + const maxDistance = 200; + const distance = Math.sqrt(directionX * directionX + directionY * directionY); + + // 计算归一化方向向量 + let normalizedX = 0; + let normalizedY = 0; + if (distance > 0) { + normalizedX = directionX / distance; + normalizedY = directionY / distance; + } + + // 计算目标位置:元歌当前位置 + 归一化方向向量 * 最大距离 + const targetX = yuange.x + normalizedX * maxDistance; + const targetY = yuange.y + normalizedY * maxDistance; + + // 创建傀儡直接在指定位置生成,不需要从本体位置开始移动 + this.puppet = new Unit(this, targetX, targetY, 'player'); + this.puppet.name = 'yuange-puppet'; + this.puppet.type = 'yuange-puppet'; + this.puppet.setScale(0.8); + this.puppet.tint = 0x00ffff; // 用蓝色区分傀儡 + this.unitGroup.add(this.puppet); + + // 切换控制到傀儡 + this.isControllingPuppet = true; + yuange.setSelected(false); + this.currentlyControlled = this.puppet; + this.controlInfoText.setText('当前控制: 元歌傀儡'); + + // 更新技能轮盘显示为傀儡的技能 + this.updateSkillWheelDisplay(); + + // 更新ControlMode的选中列表,确保虚拟摇杆能控制傀儡 + import('../../systems/ControlMode.js').then(mod => { + mod.clearSelection(); + mod.addToSelection(this.puppet); + mod.setActiveUnit(this.puppet); + // 自动锁定相机到傀儡 + mod.setCameraLocked(true); + }); + } + break; + case 2: // 技能2:扇形飞镖攻击 + this.castFanDartSkill(); + break; + case 4: // 技能4:本体向技能指向方向移动 + // 确保是本体在控制 + if (!this.isControllingPuppet) { + // 使用轮盘相对方向计算目标位置 + const maxDistance = 200; + const distance = Math.sqrt(directionX * directionX + directionY * directionY); + + // 计算归一化方向向量 + let normalizedX = 0; + let normalizedY = 0; + if (distance > 0) { + normalizedX = directionX / distance; + normalizedY = directionY / distance; + } + + // 计算目标位置:元歌当前位置 + 归一化方向向量 * 最大距离 + const targetX = yuange.x + normalizedX * maxDistance; + const targetY = yuange.y + normalizedY * maxDistance; + + // 短暂停顿(0.5秒) + yuange.setVelocity(0, 0); + + // 延迟后直接传送,不需要移动过程 + this.time.delayedCall(500, () => { + yuange.setPosition(targetX, targetY); + yuange.setVelocity(0, 0); + }, [], this); + } + break; + } + } + } + + /** + * 释放扇形飞镖技能 + */ + castFanDartSkill() { + if (!this.currentlyControlled) return; + + // 计算技能释放方向(基于技能轮盘中心到指针的相对方向) + const pointer = this.input.activePointer; + const directionX = pointer.x - this.skillWheel.centerX; + const directionY = pointer.y - this.skillWheel.centerY; + + // 计算技能释放角度 + const skillAngle = Math.atan2(directionY, directionX); + + // 扇形角度范围:30度(转换为弧度) + const fanAngle = Phaser.Math.DegToRad(30); + + // 飞镖数量 + const dartCount = 5; + + // 飞镖速度 + const dartSpeed = 1000; + + // 飞镖飞行距离 + const dartDistance = 300; + + // 计算每个飞镖的角度间隔 + const angleStep = fanAngle / (dartCount - 1); + + // 计算起始角度(扇形的左边界) + const startAngle = skillAngle - fanAngle / 2; + + // 创建飞镖 + for (let i = 0; i < dartCount; i++) { + // 计算当前飞镖的角度 + const currentAngle = startAngle + i * angleStep; + + // 计算飞镖的初始位置 + const offset = 0; // 偏移距离 + const dartX = this.currentlyControlled.x + Math.cos(currentAngle) * offset; + const dartY = this.currentlyControlled.y + Math.sin(currentAngle) * offset; + + // 创建飞镖精灵 + const dart = this.physics.add.sprite(dartX, dartY, 'dart'); + dart.setRotation(currentAngle); + + // 计算飞镖的速度向量 + const velocityX = Math.cos(currentAngle) * dartSpeed; + const velocityY = Math.sin(currentAngle) * dartSpeed; + + // 设置飞镖速度 + dart.setVelocity(velocityX, velocityY); + + // 限制飞镖飞行距离 + this.time.delayedCall(dartDistance / dartSpeed * 1000, () => { + if (dart && dart.active) { + dart.destroy(); + } + }); + + // 移除飞镖碰撞检测 + } + } + + /** + * 更新技能预览效果 + */ + updateSkillPreview() { + // 清除之前的预览 + this.clearSkillPreview(); + + if (!this.currentlyControlled) return; + + // 计算技能释放方向(基于技能轮盘中心到指针的相对方向) + const pointer = this.input.activePointer; + const directionX = pointer.x - this.skillWheel.centerX; + const directionY = pointer.y - this.skillWheel.centerY; + + // 计算技能释放角度 + const skillAngle = Math.atan2(directionY, directionX); + + // 计算归一化方向向量 + const distance = Math.sqrt(directionX * directionX + directionY * directionY); + let normalizedX = 0; + let normalizedY = 0; + if (distance > 0) { + normalizedX = directionX / distance; + normalizedY = directionY / distance; + } + + // 创建预览图形 + this.skillPreview = this.add.graphics(); + this.skillPreview.setScrollFactor(1); + this.skillPreview.setDepth(99); + + // 根据当前激活的技能显示不同的预览效果 + if (this.skillWheel.activeSkill === 1 && !this.isControllingPuppet) { + // 一技能:扔出傀儡预览 + const maxDistance = 200; + + // 计算目标位置 + const targetX = this.currentlyControlled.x + normalizedX * maxDistance; + const targetY = this.currentlyControlled.y + normalizedY * maxDistance; + + // 绘制移动路径 + this.skillPreview.lineStyle(2, 0x00ff00, 0.5); + this.skillPreview.beginPath(); + this.skillPreview.moveTo(this.currentlyControlled.x, this.currentlyControlled.y); + this.skillPreview.lineTo(targetX, targetY); + this.skillPreview.strokePath(); + + // 绘制目标位置指示器 + this.skillPreview.lineStyle(2, 0x00ff00, 1); + this.skillPreview.strokeCircle(targetX, targetY, 15); + this.skillPreview.fillStyle(0x00ff00, 0.2); + this.skillPreview.fillCircle(targetX, targetY, 15); + } else if (this.skillWheel.activeSkill === 4 && !this.isControllingPuppet) { + // 四技能:本体移动预览 + const maxDistance = 200; + + // 计算目标位置 + const targetX = this.currentlyControlled.x + normalizedX * maxDistance; + const targetY = this.currentlyControlled.y + normalizedY * maxDistance; + + // 绘制移动路径 + this.skillPreview.lineStyle(2, 0xffff00, 0.5); + this.skillPreview.beginPath(); + this.skillPreview.moveTo(this.currentlyControlled.x, this.currentlyControlled.y); + this.skillPreview.lineTo(targetX, targetY); + this.skillPreview.strokePath(); + + // 绘制目标位置指示器 + this.skillPreview.lineStyle(2, 0xffff00, 1); + this.skillPreview.strokeCircle(targetX, targetY, 15); + this.skillPreview.fillStyle(0xffff00, 0.2); + this.skillPreview.fillCircle(targetX, targetY, 15); + } else if (this.skillWheel.activeSkill === 2 && !this.isControllingPuppet) { + // 二技能:扇形飞镖攻击预览 + // 扇形角度范围:30度(转换为弧度) + const fanAngle = Phaser.Math.DegToRad(30); + + // 飞镖数量 + const dartCount = 5; + + // 飞镖飞行距离 + const dartDistance = 400; + + // 计算每个飞镖的角度间隔 + const angleStep = fanAngle / (dartCount - 1); + + // 计算起始角度(扇形的左边界) + const startAngle = skillAngle - fanAngle / 2; + + // 绘制扇形预览区域 + this.skillPreview.lineStyle(2, 0x00ffff, 0.5); + this.skillPreview.beginPath(); + this.skillPreview.moveTo(this.currentlyControlled.x, this.currentlyControlled.y); + + // 绘制扇形边界 + for (let i = 0; i <= 30; i++) { + const angle = startAngle + (i / 30) * fanAngle; + const x = this.currentlyControlled.x + Math.cos(angle) * dartDistance; + const y = this.currentlyControlled.y + Math.sin(angle) * dartDistance; + this.skillPreview.lineTo(x, y); + } + + this.skillPreview.lineTo(this.currentlyControlled.x, this.currentlyControlled.y); + this.skillPreview.strokePath(); + + // 绘制飞镖轨迹预览 + for (let i = 0; i < dartCount; i++) { + // 计算当前飞镖的角度 + const currentAngle = startAngle + i * angleStep; + + // 计算飞镖终点位置 + const endX = this.currentlyControlled.x + Math.cos(currentAngle) * dartDistance; + const endY = this.currentlyControlled.y + Math.sin(currentAngle) * dartDistance; + + // 绘制轨迹线 + this.skillPreview.lineStyle(1, 0xffff00, 0.3); + this.skillPreview.beginPath(); + this.skillPreview.moveTo(this.currentlyControlled.x, this.currentlyControlled.y); + this.skillPreview.lineTo(endX, endY); + this.skillPreview.strokePath(); + + // 绘制飞镖预览 + const previewDart = this.add.sprite(this.currentlyControlled.x, this.currentlyControlled.y, 'dart'); + previewDart.setRotation(currentAngle); + previewDart.setAlpha(0.5); + previewDart.setScrollFactor(1); + previewDart.setDepth(99); + + // 保存到预览对象中,以便后续清除 + if (!this.skillPreview.previewObjects) { + this.skillPreview.previewObjects = []; + } + this.skillPreview.previewObjects.push(previewDart); + } + } else if (this.skillWheel.activeSkill === 2 && this.isControllingPuppet) { + // 傀儡二技能:交换位置预览 + const yuange = this.characters.find(char => char.name === 'yuange'); + if (yuange) { + // 绘制交换位置路径 + this.skillPreview.lineStyle(2, 0x00ffff, 0.5); + this.skillPreview.beginPath(); + this.skillPreview.moveTo(this.currentlyControlled.x, this.currentlyControlled.y); + this.skillPreview.lineTo(yuange.x, yuange.y); + this.skillPreview.strokePath(); + + // 绘制双向箭头,表示交换 + this.skillPreview.lineStyle(2, 0x00ffff, 1); + this.skillPreview.beginPath(); + // 箭头1 + const arrow1X = this.currentlyControlled.x + (yuange.x - this.currentlyControlled.x) * 0.3; + const arrow1Y = this.currentlyControlled.y + (yuange.y - this.currentlyControlled.y) * 0.3; + this.skillPreview.moveTo(arrow1X, arrow1Y); + this.skillPreview.lineTo(arrow1X + 10, arrow1Y - 10); + this.skillPreview.moveTo(arrow1X, arrow1Y); + this.skillPreview.lineTo(arrow1X - 10, arrow1Y - 10); + // 箭头2 + const arrow2X = this.currentlyControlled.x + (yuange.x - this.currentlyControlled.x) * 0.7; + const arrow2Y = this.currentlyControlled.y + (yuange.y - this.currentlyControlled.y) * 0.7; + this.skillPreview.moveTo(arrow2X, arrow2Y); + this.skillPreview.lineTo(arrow2X + 10, arrow2Y + 10); + this.skillPreview.moveTo(arrow2X, arrow2Y); + this.skillPreview.lineTo(arrow2X - 10, arrow2Y + 10); + this.skillPreview.strokePath(); + } + } else if (this.skillWheel.activeSkill === 4 && this.isControllingPuppet) { + // 傀儡四技能:傀儡移动预览 + const maxDistance = 300; + + // 计算目标位置 + const targetX = this.currentlyControlled.x + normalizedX * maxDistance; + const targetY = this.currentlyControlled.y + normalizedY * maxDistance; + + // 绘制移动路径 + this.skillPreview.lineStyle(2, 0x00ffff, 0.5); + this.skillPreview.beginPath(); + this.skillPreview.moveTo(this.currentlyControlled.x, this.currentlyControlled.y); + this.skillPreview.lineTo(targetX, targetY); + this.skillPreview.strokePath(); + + // 绘制目标位置指示器 + this.skillPreview.lineStyle(2, 0x00ffff, 1); + this.skillPreview.strokeCircle(targetX, targetY, 15); + this.skillPreview.fillStyle(0x00ffff, 0.2); + this.skillPreview.fillCircle(targetX, targetY, 15); + } + } + + /** + * 清除技能预览效果 + */ + clearSkillPreview() { + if (this.skillPreview) { + // 清除预览对象 + if (this.skillPreview.previewObjects) { + this.skillPreview.previewObjects.forEach(obj => { + if (obj && obj.active) { + obj.destroy(); + } + }); + } + + // 清除预览图形 + this.skillPreview.destroy(); + this.skillPreview = null; + } + } + + /** + * 释放貂蝉技能 + * @param {number} skillIndex 技能索引(1-4) + */ + castDiaochanSkill(skillIndex) { + const diaochan = this.currentlyControlled; + + // 计算技能释放方向(基于技能轮盘中心到指针的相对方向) + const pointer = this.input.activePointer; + // 计算轮盘中心到指针的方向向量 + const directionX = pointer.x - this.skillWheel.centerX; + const directionY = pointer.y - this.skillWheel.centerY; + + switch (skillIndex) { + case 2: // 技能2:向指定方向移动 + // 使用轮盘相对方向计算移动方向 + const maxDistance = 250; + const distance = Math.sqrt(directionX * directionX + directionY * directionY); + let ratio = maxDistance / distance; + if (isNaN(ratio) || ratio > 1) ratio = 1; + + // 计算移动距离 + const moveX = directionX * ratio; + const moveY = directionY * ratio; + + // 立即向目标方向移动一定距离 + diaochan.x += moveX; + diaochan.y += moveY; + break; + } + } + + /** + * 切换回元歌本体 + */ + switchToYuange() { + if (this.puppet) { + // 删除傀儡 + this.puppet.destroy(); + this.puppet = null; + } + + // 切换回元歌本体控制 + this.isControllingPuppet = false; + this.currentlyControlled = this.characters.find(char => char.name === 'yuange'); + this.currentlyControlled.setSelected(true); + + // 更新UI提示 + this.controlInfoText.setText('当前控制: 元歌'); + + // 更新技能轮盘显示为元歌本体的技能 + this.updateSkillWheelDisplay(); + + // 更新ControlMode的选中列表,确保虚拟摇杆能控制本体 + import('../../systems/ControlMode.js').then(mod => { + mod.clearSelection(); + mod.addToSelection(this.currentlyControlled); + mod.setActiveUnit(this.currentlyControlled); + // 自动锁定相机到本体 + mod.setCameraLocked(true); + }); + } + + /** + * 窗口大小变化时的处理函数 + * 当浏览器窗口大小改变时,调整游戏画布的大小以适应新窗口 + */ + resizeGame() { + // 调用Phaser的scale.resize方法,将游戏画布大小调整为当前窗口大小 + this.scale.resize(window.innerWidth, window.innerHeight); + + // 更新UI位置 + if (this.skillInfoText) { + this.skillInfoText.setPosition(10, window.innerHeight - 100); + } + if (this.controlInfoText) { + this.controlInfoText.setPosition(10, window.innerHeight - 60); + } + } +} \ No newline at end of file