From f86e5f3a7938bcb28742c94a3aa7fcb6bc91454b Mon Sep 17 00:00:00 2001 From: chicrobot Date: Tue, 30 Dec 2025 21:45:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(control)=20=E5=A4=9A=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/gameConfig.js | 17 +++++++ index.html | 12 +++++ main.js | 9 ++++ objects/Unit.js | 16 +++++++ scenes/GameScene.js | 26 +++++++++++ systems/CommandSystem.js | 31 +++++++++++++ systems/ControlMode.js | 24 ++++++++++ systems/InputSystem.js | 86 ++++++++++++++++++++++++++++++++++++ systems/SelectionSystem.js | 42 ++++++++++++++++++ ui/VirtualJoystick.js | 78 +++++++++++++++++++++++++++++++++ utils/phaser-demo.html | 90 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 431 insertions(+) create mode 100644 config/gameConfig.js create mode 100644 index.html create mode 100644 main.js create mode 100644 objects/Unit.js create mode 100644 scenes/GameScene.js create mode 100644 systems/CommandSystem.js create mode 100644 systems/ControlMode.js create mode 100644 systems/InputSystem.js create mode 100644 systems/SelectionSystem.js create mode 100644 ui/VirtualJoystick.js create mode 100644 utils/phaser-demo.html diff --git a/config/gameConfig.js b/config/gameConfig.js new file mode 100644 index 0000000..1dc1e20 --- /dev/null +++ b/config/gameConfig.js @@ -0,0 +1,17 @@ +export default { + type: Phaser.AUTO, + width: window.innerWidth, + height: window.innerHeight, + backgroundColor: '#87ceeb', + physics: { + default: 'arcade', + arcade: { + gravity: { y: 0 }, + debug: false + } + }, + scale: { + mode: Phaser.Scale.RESIZE, + autoCenter: Phaser.Scale.NO_CENTER + } +}; \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..acdd3e5 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + Multi-Control Demo + + + + + + + \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..11fee11 --- /dev/null +++ b/main.js @@ -0,0 +1,9 @@ +import gameConfig from './config/gameConfig.js'; +import GameScene from './scenes/GameScene.js'; + +const config = { + ...gameConfig, + scene: [GameScene] +}; + +window.game = new Phaser.Game(config); \ No newline at end of file diff --git a/objects/Unit.js b/objects/Unit.js new file mode 100644 index 0000000..17b4697 --- /dev/null +++ b/objects/Unit.js @@ -0,0 +1,16 @@ +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.setCollideWorldBounds(true); + } + + moveTo(targetX, targetY) { + this.scene.physics.moveToObject(this, {x: targetX, y: targetY}, 200); + } + + moveRelative(dirX, dirY, speed = 100) { + this.setVelocity(dirX * speed, dirY * speed); + } +} \ No newline at end of file diff --git a/scenes/GameScene.js b/scenes/GameScene.js new file mode 100644 index 0000000..e620e95 --- /dev/null +++ b/scenes/GameScene.js @@ -0,0 +1,26 @@ +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'; + +export default class GameScene extends Phaser.Scene { + create() { + this.unitGroup = this.add.group(); + for (let i = 0; i < 5; i++) { + const unit = new Unit(this, 100 + i * 80, 300, 'player'); + this.unitGroup.add(unit); + } + + this.inputSystem = new InputSystem(this); + this.selectionSystem = new SelectionSystem(this); + this.commandSystem = new CommandSystem(this); + + // 初始化虚拟摇杆 + this.virtualJoystick = new VirtualJoystick(this); + } + + update() { + this.inputSystem.update(); + } +} \ No newline at end of file diff --git a/systems/CommandSystem.js b/systems/CommandSystem.js new file mode 100644 index 0000000..9ee4e42 --- /dev/null +++ b/systems/CommandSystem.js @@ -0,0 +1,31 @@ +// systems/CommandSystem.js +import { activeUnit, selectedUnits } from './ControlMode.js'; + +export class CommandSystem { + constructor(scene) { + this.scene = scene; + + scene.events.on('move-relative', this.onMoveRelative, this); + scene.events.on('move-to-point', this.onMoveToPoint, this); + scene.events.on('select-unit', this.onSelectUnit, this); + } + + onMoveRelative(dir) { + if (activeUnit) { + activeUnit.moveRelative(dir.x, dir.y); + } + } + + onMoveToPoint(x, y) { + const targets = selectedUnits.length > 0 ? selectedUnits : (activeUnit ? [activeUnit] : []); + targets.forEach(unit => unit.moveTo(x, y)); + } + + onSelectUnit(unit) { + // 切换单位 + import('./ControlMode.js').then(mod => { + mod.setActiveUnit(unit); + this.scene.events.emit('unit-activated', unit); + }); + } +} \ No newline at end of file diff --git a/systems/ControlMode.js b/systems/ControlMode.js new file mode 100644 index 0000000..9f3b3ae --- /dev/null +++ b/systems/ControlMode.js @@ -0,0 +1,24 @@ +export const CONTROL_MODE = { + SINGLE: 'single', + GROUP: 'group', + JOYSTICK: 'joystick' +}; + +export let activeUnit = null; +export let selectedUnits = []; + +export function setActiveUnit(unit) { + activeUnit = unit; + selectedUnits = unit ? [unit] : []; +} + +export function addToSelection(unit) { + if (!selectedUnits.includes(unit)) { + selectedUnits.push(unit); + } +} + +export function clearSelection() { + activeUnit = null; + selectedUnits = []; +} \ No newline at end of file diff --git a/systems/InputSystem.js b/systems/InputSystem.js new file mode 100644 index 0000000..1f1d973 --- /dev/null +++ b/systems/InputSystem.js @@ -0,0 +1,86 @@ +// systems/InputSystem.js +import { clearSelection, setActiveUnit } from './ControlMode.js'; + +export class InputSystem { + constructor(scene) { + this.scene = scene; + this.selectionStart = null; + + // 初始化键盘 + const kb = scene.input.keyboard; + this.keys = { + up: [kb.addKey(Phaser.Input.Keyboard.KeyCodes.W), kb.addKey(Phaser.Input.Keyboard.KeyCodes.UP)], + down: [kb.addKey(Phaser.Input.Keyboard.KeyCodes.S), kb.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN)], + left: [kb.addKey(Phaser.Input.Keyboard.KeyCodes.A), kb.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT)], + right: [kb.addKey(Phaser.Input.Keyboard.KeyCodes.D), kb.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT)] + }; + + // 鼠标/触摸事件 + scene.input.on('pointerdown', this.onPointerDown, this); + scene.input.on('pointerup', this.onPointerUp, this); + } + + isKeyDown(keyGroup) { + return keyGroup.some(key => key.isDown); + } + + update() { + // 键盘相对移动(仅当有激活单位) + const dir = { x: 0, y: 0 }; + if (this.isKeyDown(this.keys.left)) dir.x -= 1; + if (this.isKeyDown(this.keys.right)) dir.x += 1; + 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); + } + } + + onPointerDown(pointer) { + if (pointer.leftButtonDown()) { + const worldX = pointer.worldX; + const worldY = pointer.worldY; + + // 检查是否点中某个单位 + let clickedUnit = null; + this.scene.unitGroup.getChildren().forEach(unit => { + const bounds = unit.getBounds(); + if (worldX >= bounds.x && worldX <= bounds.right && + worldY >= bounds.y && worldY <= bounds.bottom) { + clickedUnit = unit; + } + }); + + if (clickedUnit) { + // 切换控制目标 + this.scene.events.emit('select-unit', clickedUnit); + } else { + // 开始框选或移动指令 + this.selectionStart = { x: pointer.x, y: pointer.y }; + this.scene.events.emit('selection-start', this.selectionStart); + } + } + } + + onPointerUp(pointer) { + if (this.selectionStart) { + 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) + ); + + // 如果框太小,视为“点击地面移动” + if (rect.width < 10 && rect.height < 10) { + this.scene.events.emit('move-to-point', pointer.worldX, pointer.worldY); + } else { + // 否则执行框选 + this.scene.events.emit('selection-end', rect); + } + + this.selectionStart = null; + } + } +} \ No newline at end of file diff --git a/systems/SelectionSystem.js b/systems/SelectionSystem.js new file mode 100644 index 0000000..4139834 --- /dev/null +++ b/systems/SelectionSystem.js @@ -0,0 +1,42 @@ +// systems/SelectionSystem.js +import { clearSelection, addToSelection, setActiveUnit } from './ControlMode.js'; + +export class SelectionSystem { + constructor(scene) { + this.scene = scene; + this.selectionBox = null; + + scene.events.on('selection-start', this.onStart, this); + scene.events.on('selection-end', this.onEnd, this); + } + + onStart(pos) { + if (!this.selectionBox) { + this.selectionBox = this.scene.add.rectangle(pos.x, pos.y, 1, 1, 0x00ff00, 0.3) + .setStrokeStyle(1, 0xffffff) + .setVisible(true); + } else { + this.selectionBox.setPosition(pos.x, pos.y).setSize(1, 1).setVisible(true); + } + } + + onEnd(rect) { + if (this.selectionBox) { + this.selectionBox.setVisible(false); + } + + const units = this.scene.unitGroup.getChildren(); + const selected = units.filter(unit => { + const unitRect = unit.getBounds(); + return Phaser.Geom.Rectangle.Overlaps(rect, unitRect); + }); + + clearSelection(); + selected.forEach(addToSelection); + + if (selected.length > 0) { + setActiveUnit(selected[0]); // 激活第一个 + this.scene.events.emit('units-selected', selected); + } + } +} \ No newline at end of file diff --git a/ui/VirtualJoystick.js b/ui/VirtualJoystick.js new file mode 100644 index 0000000..c18bdec --- /dev/null +++ b/ui/VirtualJoystick.js @@ -0,0 +1,78 @@ +// ui/VirtualJoystick.js +export class VirtualJoystick { + constructor(scene) { + this.scene = scene; + this.isActive = false; + this.base = null; + this.knob = null; + this.pointer = null; + this.centerX = 0; + this.centerY = 0; + this.radius = 50; + + // 仅在触屏设备显示 + if ('ontouchstart' in window) { + this.create(); + this.bindEvents(); + } + } + + create() { + const x = 100; + const y = this.scene.scale.height - 100; + this.centerX = x; + this.centerY = y; + + this.base = this.scene.add.circle(x, y, this.radius, 0x444444, 0.5); + this.knob = this.scene.add.circle(x, y, this.radius * 0.5, 0xff0000, 0.8); + + this.base.setScrollFactor(0); // 不随相机移动 + this.knob.setScrollFactor(0); + } + + bindEvents() { + this.scene.input.on('pointerdown', (ptr) => { + if (this.isInBase(ptr)) { + this.pointer = ptr; + this.isActive = true; + } + }); + + this.scene.input.on('pointermove', (ptr) => { + if (this.isActive && this.pointer === ptr) { + const dx = ptr.x - this.centerX; + const dy = ptr.y - this.centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance > this.radius) { + // 限制在圆内 + const angle = Math.atan2(dy, dx); + ptr.x = this.centerX + Math.cos(angle) * this.radius; + ptr.y = this.centerY + Math.sin(angle) * this.radius; + } + + this.knob.setPosition(ptr.x, ptr.y); + + // 发送相对方向(归一化) + const dirX = dx / this.radius; + const dirY = dy / this.radius; + this.scene.events.emit('move-relative', { x: dirX, y: dirY }); + } + }); + + this.scene.input.on('pointerup', (ptr) => { + if (this.pointer === ptr) { + this.isActive = false; + this.pointer = null; + this.knob.setPosition(this.centerX, this.centerY); + this.scene.events.emit('move-relative', { x: 0, y: 0 }); // 停止 + } + }); + } + + isInBase(pointer) { + const dx = pointer.x - this.centerX; + const dy = pointer.y - this.centerY; + return Math.sqrt(dx * dx + dy * dy) <= this.radius + 20; // 容差 + } +} \ No newline at end of file diff --git a/utils/phaser-demo.html b/utils/phaser-demo.html new file mode 100644 index 0000000..59256c6 --- /dev/null +++ b/utils/phaser-demo.html @@ -0,0 +1,90 @@ + + + + + Phaser Top-Down Demo + + + + + + + + \ No newline at end of file