From e2bc630d6d1abde5bad69c80baacd25b37e6eb7a Mon Sep 17 00:00:00 2001 From: chicrobot Date: Wed, 31 Dec 2025 00:30:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(control)=20=E5=B0=8F=E5=9C=B0=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/gameConfig.js | 5 +- index.html | 19 +++++- objects/Unit.js | 116 +++++++++++++++++++++++++++++++++++-- scenes/GameScene.js | 63 +++++++++++++++++++- systems/CommandSystem.js | 14 +++-- systems/ControlMode.js | 2 +- systems/InputSystem.js | 91 ++++++++++++++++++++++++++--- systems/SelectionSystem.js | 53 ++++++++++++++++- 8 files changed, 339 insertions(+), 24 deletions(-) diff --git a/config/gameConfig.js b/config/gameConfig.js index 1dc1e20..d05eaef 100644 --- a/config/gameConfig.js +++ b/config/gameConfig.js @@ -12,6 +12,9 @@ export default { }, scale: { mode: Phaser.Scale.RESIZE, - autoCenter: Phaser.Scale.NO_CENTER + autoCenter: Phaser.Scale.NO_CENTER, + expandParent: true, + width: window.innerWidth, + height: window.innerHeight } }; \ No newline at end of file diff --git a/index.html b/index.html index acdd3e5..f186ecd 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,24 @@ Multi-Control Demo - + diff --git a/objects/Unit.js b/objects/Unit.js index 17b4697..3cf3d6b 100644 --- a/objects/Unit.js +++ b/objects/Unit.js @@ -1,16 +1,124 @@ +// objects/Unit.js export default class Unit extends Phaser.Physics.Arcade.Sprite { constructor(scene, x, y, texture) { super(scene, x, y, texture); scene.add.existing(this); scene.physics.add.existing(this); + + // 启用圆形碰撞体(更自然) + this.setCircle(14); + + // 启用与世界边界的碰撞(必须配合 physics.world.setBounds) this.setCollideWorldBounds(true); + + // 移动目标(x, y) + this.target = null; + + // 选中状态 + this.isSelected = false; + this.selectedIndicator = null; + this.createSelectedIndicator(); } - moveTo(targetX, targetY) { - this.scene.physics.moveToObject(this, {x: targetX, y: targetY}, 200); + /** + * 创建选中状态指示器 + */ + createSelectedIndicator() { + // 创建一个外圈指示器,默认隐藏 + this.selectedIndicator = this.scene.add.circle(this.x, this.y, this.width * 0.7, 0x00ff00, 0.0); + this.selectedIndicator.setStrokeStyle(2, 0x00ff00, 1); + this.selectedIndicator.setVisible(false); + + // 确保指示器与单位一起移动 + this.selectedIndicator.setOrigin(0.5, 0.5); + // 移除固定位置设置,让指示器随单位和相机一起移动 + this.selectedIndicator.setScrollFactor(1, 1); } - moveRelative(dirX, dirY, speed = 100) { - this.setVelocity(dirX * speed, dirY * speed); + /** + * 设置单位选中状态 + * @param {boolean} isSelected + */ + setSelected(isSelected) { + this.isSelected = isSelected; + if (this.selectedIndicator) { + this.selectedIndicator.setVisible(isSelected); + } + } + + /** + * 设置移动目标点 + * @param {number} x + * @param {number} y + */ + setTarget(x, y) { + // 获取世界边界 + const worldBounds = this.scene.physics.world.bounds; + + // 限制目标点在世界边界内 + const clampedX = Math.max(worldBounds.x, Math.min(worldBounds.right, x)); + const clampedY = Math.max(worldBounds.y, Math.min(worldBounds.bottom, y)); + + this.target = { x: clampedX, y: clampedY }; + } + + /** + * 清除目标并停止移动 + */ + clearTarget() { + this.target = null; + this.setVelocity(0, 0); + } + + /** + * 相对移动控制(用于键盘/虚拟摇杆) + * @param {number} x - 水平方向 (-1 到 1) + * @param {number} y - 垂直方向 (-1 到 1) + */ + moveRelative(x, y) { + const speed = 150; // 单位:像素/秒 + // 归一化方向向量 + const magnitude = Math.sqrt(x * x + y * y); + if (magnitude > 0) { + // 有按键输入时,清除目标点,切换到直接控制模式 + this.clearTarget(); + const normalizedX = x / magnitude; + const normalizedY = y / magnitude; + this.setVelocity(normalizedX * speed, normalizedY * speed); + } else { + // 没有按键输入时,保持当前的目标点(如果有的话) + this.setVelocity(0, 0); + } + } + + /** + * 每帧更新移动逻辑(需在 GameScene.update 中调用) + */ + update() { + // 更新选中指示器位置 + if (this.selectedIndicator && this.isSelected) { + this.selectedIndicator.setPosition(this.x, this.y); + } + } + + updateMovement() { + if (!this.target) { + return; + } + + const dx = this.target.x - this.x; + const dy = this.target.y - this.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + // 到达阈值:5 像素内视为“到达” + if (distance < 5) { + // 精确对齐到目标点(可选) + this.setPosition(this.target.x, this.target.y); + this.clearTarget(); + } else { + const speed = 150; // 单位:像素/秒 + const angle = Math.atan2(dy, dx); + this.setVelocity(Math.cos(angle) * speed, Math.sin(angle) * speed); + } } } \ No newline at end of file diff --git a/scenes/GameScene.js b/scenes/GameScene.js index e620e95..34b6c1e 100644 --- a/scenes/GameScene.js +++ b/scenes/GameScene.js @@ -1,26 +1,83 @@ +// scenes/GameScene.js 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'; export default class GameScene extends Phaser.Scene { create() { + // === 0. 添加窗口大小变化监听 === + window.addEventListener('resize', this.resizeGame.bind(this)); + + // === 1. 创建玩家纹理(红圈)=== + if (!this.textures.exists('player')) { + const canvas = document.createElement('canvas'); + canvas.width = 32; + canvas.height = 32; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#ff0000'; + ctx.beginPath(); + ctx.arc(16, 16, 14, 0, Math.PI * 2); + ctx.fill(); + this.textures.addCanvas('player', canvas); + } + + // === 2. 扩大物理世界和相机边界(关键!)=== + const worldSize = 2000; + this.physics.world.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + + // === 3. 创建单位组 === this.unitGroup = this.add.group(); + for (let i = 0; i < 5; i++) { - const unit = new Unit(this, 100 + i * 80, 300, 'player'); + const worldSizePersent = worldSize/100 + const unit = new Unit(this, worldSizePersent + 2*i * worldSizePersent, 2 * worldSizePersent, 'player'); this.unitGroup.add(unit); } + // === 4. 初始化系统 === this.inputSystem = new InputSystem(this); this.selectionSystem = new SelectionSystem(this); this.commandSystem = new CommandSystem(this); - - // 初始化虚拟摇杆 this.virtualJoystick = new VirtualJoystick(this); + + // === 5. 添加角色之间的碰撞检测 === + this.physics.add.collider(this.unitGroup, this.unitGroup); + + // === 6. 创建小地图 === + this.miniMap = new MiniMap(this, { + worldSize: worldSize, + mapSize: 200, + x: 10, + y: 10 + }); } update() { + // 更新输入(键盘、鼠标) this.inputSystem.update(); + + // 更新所有单位的移动逻辑和选中状态(关键!) + this.unitGroup.getChildren().forEach(unit => { + if (unit.update) { + unit.update(); + } + unit.updateMovement(); + }); + + // 更新小地图 + if (this.miniMap) { + this.miniMap.update(); + } + } + + /** + * 窗口大小变化时的处理函数 + */ + resizeGame() { + this.scale.resize(window.innerWidth, window.innerHeight); } } \ No newline at end of file diff --git a/systems/CommandSystem.js b/systems/CommandSystem.js index 9ee4e42..f79e1f3 100644 --- a/systems/CommandSystem.js +++ b/systems/CommandSystem.js @@ -11,14 +11,20 @@ export class CommandSystem { } onMoveRelative(dir) { - if (activeUnit) { - activeUnit.moveRelative(dir.x, dir.y); - } + const targets = selectedUnits.length > 0 ? selectedUnits : (activeUnit ? [activeUnit] : []); + + targets.forEach(unit => { + unit.moveRelative(dir.x, dir.y); + }); } onMoveToPoint(x, y) { const targets = selectedUnits.length > 0 ? selectedUnits : (activeUnit ? [activeUnit] : []); - targets.forEach(unit => unit.moveTo(x, y)); + + + targets.forEach(unit => { + unit.setTarget(x, y); // ← 使用新接口 + }); } onSelectUnit(unit) { diff --git a/systems/ControlMode.js b/systems/ControlMode.js index 9f3b3ae..a11061c 100644 --- a/systems/ControlMode.js +++ b/systems/ControlMode.js @@ -9,7 +9,7 @@ export let selectedUnits = []; export function setActiveUnit(unit) { activeUnit = unit; - selectedUnits = unit ? [unit] : []; + // 不再重置selectedUnits数组,这样可以同时选中多个单位 } export function addToSelection(unit) { diff --git a/systems/InputSystem.js b/systems/InputSystem.js index 1f1d973..26b93d7 100644 --- a/systems/InputSystem.js +++ b/systems/InputSystem.js @@ -15,8 +15,12 @@ export class InputSystem { right: [kb.addKey(Phaser.Input.Keyboard.KeyCodes.D), kb.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT)] }; + // 禁用浏览器右键菜单 + scene.input.mouse.disableContextMenu(); + // 鼠标/触摸事件 scene.input.on('pointerdown', this.onPointerDown, this); + scene.input.on('pointermove', this.onPointerMove, this); scene.input.on('pointerup', this.onPointerUp, this); } @@ -32,12 +36,17 @@ export class InputSystem { if (this.isKeyDown(this.keys.up)) dir.y -= 1; if (this.isKeyDown(this.keys.down)) dir.y += 1; - if (dir.x !== 0 || dir.y !== 0) { - this.scene.events.emit('move-relative', dir); - } + // 无论是否有按键按下,都发送移动事件 + // 当dir.x和dir.y都为0时,单位会停止移动 + this.scene.events.emit('move-relative', dir); } onPointerDown(pointer) { + // 检查鼠标是否在小地图上,如果是则不处理框选 + if (this.isMouseOnMiniMap(pointer)) { + return; + } + if (pointer.leftButtonDown()) { const worldX = pointer.worldX; const worldY = pointer.worldY; @@ -57,19 +66,59 @@ export class InputSystem { this.scene.events.emit('select-unit', clickedUnit); } else { // 开始框选或移动指令 - this.selectionStart = { x: pointer.x, y: pointer.y }; + // 存储世界坐标用于框选 + this.selectionStart = { x: worldX, y: worldY }; this.scene.events.emit('selection-start', this.selectionStart); } + } else if (pointer.rightButtonDown()) { + // 右键点击:移动选中的单位 + const worldX = pointer.worldX; + const worldY = pointer.worldY; + this.scene.events.emit('move-to-point', worldX, worldY); + } + } + + onPointerMove(pointer) { + // 检查鼠标是否在小地图上,如果是则不处理框选 + if (this.isMouseOnMiniMap(pointer)) { + return; + } + + if (this.selectionStart) { + // 使用世界坐标计算框选区域 + const currentWorldX = pointer.worldX; + const currentWorldY = pointer.worldY; + + // 计算当前选择框的位置和大小 + const rect = new Phaser.Geom.Rectangle( + Math.min(this.selectionStart.x, currentWorldX), + Math.min(this.selectionStart.y, currentWorldY), + Math.abs(currentWorldX - this.selectionStart.x), + Math.abs(currentWorldY - this.selectionStart.y) + ); + + // 更新选择框的位置和大小 + this.scene.events.emit('selection-update', rect); } } onPointerUp(pointer) { + // 检查鼠标是否在小地图上,如果是则不处理框选 + if (this.isMouseOnMiniMap(pointer)) { + this.selectionStart = null; + return; + } + if (this.selectionStart) { + // 使用世界坐标计算最终框选区域 + const currentWorldX = pointer.worldX; + const currentWorldY = pointer.worldY; + const rect = new Phaser.Geom.Rectangle( - Math.min(this.selectionStart.x, pointer.x), - Math.min(this.selectionStart.y, pointer.y), - Math.abs(pointer.x - this.selectionStart.x), - Math.abs(pointer.y - this.selectionStart.y) + Math.min(this.selectionStart.x, currentWorldX), + Math.min(this.selectionStart.y, currentWorldY), + Math.abs(currentWorldX - this.selectionStart.x), + Math.abs(currentWorldY - this.selectionStart.y) ); // 如果框太小,视为“点击地面移动” @@ -83,4 +132,30 @@ export class InputSystem { this.selectionStart = null; } } + + /** + * 检查鼠标是否在小地图上 + * @param {Phaser.Input.Pointer} pointer + * @returns {boolean} + */ + isMouseOnMiniMap(pointer) { + // 检查场景中是否有小地图 + if (!this.scene.miniMap) { + return false; + } + + const miniMap = this.scene.miniMap; + const mouseX = pointer.x; + const mouseY = pointer.y; + + // 计算小地图的边界 + const mapLeft = miniMap.x; + const mapRight = miniMap.x + miniMap.mapSize; + const mapTop = miniMap.y; + const mapBottom = miniMap.y + miniMap.mapSize; + + // 检查鼠标是否在小地图边界内 + return mouseX >= mapLeft && mouseX <= mapRight && + mouseY >= mapTop && mouseY <= mapBottom; + } } \ No newline at end of file diff --git a/systems/SelectionSystem.js b/systems/SelectionSystem.js index 4139834..38fe6d7 100644 --- a/systems/SelectionSystem.js +++ b/systems/SelectionSystem.js @@ -7,7 +7,9 @@ export class SelectionSystem { this.selectionBox = null; scene.events.on('selection-start', this.onStart, this); + scene.events.on('selection-update', this.onUpdate, this); scene.events.on('selection-end', this.onEnd, this); + scene.events.on('select-unit', this.onSelectUnit, this); } onStart(pos) { @@ -20,6 +22,37 @@ export class SelectionSystem { } } + onUpdate(rect) { + if (this.selectionBox && this.selectionBox.visible) { + // 更新选择框的位置和大小 + this.selectionBox.setPosition(rect.centerX, rect.centerY); + this.selectionBox.setSize(rect.width, rect.height); + } + } + + onSelectUnit(unit) { + // 清除所有单位的选中状态 + this.scene.unitGroup.getChildren().forEach(u => { + if (u.setSelected) { + u.setSelected(false); + } + }); + + // 清除当前选择 + clearSelection(); + + // 选择点击的单位 + setActiveUnit(unit); + addToSelection(unit); + + // 设置选中状态 + if (unit.setSelected) { + unit.setSelected(true); + } + + this.scene.events.emit('units-selected', [unit]); + } + onEnd(rect) { if (this.selectionBox) { this.selectionBox.setVisible(false); @@ -30,12 +63,28 @@ export class SelectionSystem { const unitRect = unit.getBounds(); return Phaser.Geom.Rectangle.Overlaps(rect, unitRect); }); + + + + // 先清除所有单位的选中状态 + units.forEach(unit => { + if (unit.setSelected) { + unit.setSelected(false); + } + }); clearSelection(); - selected.forEach(addToSelection); if (selected.length > 0) { - setActiveUnit(selected[0]); // 激活第一个 + // 先设置激活单位 + setActiveUnit(selected[0]); + // 然后添加所有选中的单位(包括激活单位) + selected.forEach(unit => { + addToSelection(unit); + if (unit.setSelected) { + unit.setSelected(true); + } + }); this.scene.events.emit('units-selected', selected); } }