相机
平移、缩放、聚焦。观察世界的窗口。
目标
相机必须做到:
- 🖱️ 操作直觉(拖拽平移、滚轮缩放)
- 🎯 支持聚焦(居中显示 NPC 或地点)
- 🔍 支持 0.5x 到 3x 缩放
- 📱 兼容触摸设备(双指缩放、拖拽平移)
实现
一个简单的 Camera 类,封装 PixiJS Container 的变换:
ts
class Camera {
x = 0;
y = 0;
zoom = 1;
target: Container;
constructor(target: Container) {
this.target = target;
}
pan(dx: number, dy: number) {
this.x += dx;
this.y += dy;
this.apply();
}
setZoom(z: number, focusX?: number, focusY?: number) {
const oldZoom = this.zoom;
this.zoom = clamp(z, 0.5, 3);
if (focusX !== undefined && focusY !== undefined) {
this.x = focusX - (focusX - this.x) * (this.zoom / oldZoom);
this.y = focusY - (focusY - this.y) * (this.zoom / oldZoom);
}
this.apply();
}
focusOn(wx: number, wy: number, viewportWidth: number, viewportHeight: number) {
this.x = wx * TILE_SIZE - viewportWidth / 2;
this.y = wy * TILE_SIZE - viewportHeight / 2;
this.apply();
}
private apply() {
this.target.x = -this.x * this.zoom;
this.target.y = -this.y * this.zoom;
this.target.scale.set(this.zoom);
}
}输入处理
鼠标 / 触摸
ts
let isDragging = false;
let lastX = 0, lastY = 0;
stage.on('pointerdown', (event) => {
isDragging = true;
lastX = event.global.x;
lastY = event.global.y;
});
stage.on('pointermove', (event) => {
if (!isDragging) return;
camera.pan(lastX - event.global.x, lastY - event.global.y);
lastX = event.global.x;
lastY = event.global.y;
});
stage.on('pointerup', () => isDragging = false);
window.addEventListener('wheel', (event) => {
event.preventDefault();
const delta = -event.deltaY * 0.001;
camera.setZoom(camera.zoom + delta, event.clientX, event.clientY);
});触摸(双指缩放)
ts
let touchStartDist = 0;
let touchStartZoom = 1;
stage.on('touchstart', (event) => {
if (event.touches.length === 2) {
const [a, b] = event.touches;
touchStartDist = distance(a, b);
touchStartZoom = camera.zoom;
}
});
stage.on('touchmove', (event) => {
if (event.touches.length === 2) {
const [a, b] = event.touches;
const dist = distance(a, b);
camera.setZoom(touchStartZoom * (dist / touchStartDist));
}
});平滑过渡
"聚焦 NPC" 操作时,相机会平滑动画过渡:
ts
async function smoothFocusOn(camera: Camera, wx: number, wy: number, duration = 600) {
const startX = camera.x;
const startY = camera.y;
const startZoom = camera.zoom;
const targetZoom = 2;
const targetX = wx * TILE_SIZE - viewport.width / 2;
const targetY = wy * TILE_SIZE - viewport.height / 2;
return new Promise<void>(resolve => {
const start = performance.now();
const tick = (now: number) => {
const t = Math.min((now - start) / duration, 1);
const eased = easeInOutCubic(t);
camera.x = startX + (targetX - startX) * eased;
camera.y = startY + (targetY - startY) * eased;
camera.zoom = startZoom + (targetZoom - startZoom) * eased;
camera.apply();
if (t < 1) requestAnimationFrame(tick);
else resolve();
};
requestAnimationFrame(tick);
});
}小地图
屏幕角落的全局概览,显示所有建筑和 NPC 位置。点击小地图可移动主相机。
相机状态
| 状态 | 显示内容 |
|---|---|
overview | 全镇缩略图 |
focus-npc | 居中某个 NPC,缩放 2x |
focus-location | 居中某栋建筑,缩放 2x |
键盘快捷键
| 按键 | 操作 |
|---|---|
| 方向键 | 平移 |
+ / - | 放大 / 缩小 |
0 | 重置为概览 |
F | 聚焦选中的 NPC |
Esc | 取消选中 |
性能
相机不会触发重新渲染——只变换已有精灵的坐标。平移/缩放操作在常规速度下零 FPS 开销。