feat(control) 小地图

This commit is contained in:
chicrobot committed 2025-12-31 00:30:38 +08:00
1 parent f86e5f3a79
commit e2bc630d6d
8 files changed
+339 -24

No files matched your search

+4 -1
View File
@@ -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
}
};
+18 -1
View File
@@ -3,7 +3,24 @@
<head>
<meta charset="UTF-8" />
<title>Multi-Control Demo</title>
<style>body { margin: 0; }</style>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<script src="./phaser.min.js"></script>
+112 -4
View File
@@ -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);
}
}
}
+60 -3
View File
@@ -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);
}
}
+10 -4
View File
@@ -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) {
+1 -1
View File
@@ -9,7 +9,7 @@ export let selectedUnits = [];
export function setActiveUnit(unit) {
activeUnit = unit;
selectedUnits = unit ? [unit] : [];
// 不再重置selectedUnits数组,这样可以同时选中多个单位
}
export function addToSelection(unit) {
+83 -8
View File
@@ -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;
}
}
+51 -2
View File
@@ -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);
@@ -31,11 +64,27 @@ export class SelectionSystem {
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);
}
}