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