Pixi.js 游戏开发教程 / 输入交互(事件系统)
输入交互(事件系统)
概述
用户输入是交互式应用的核心。Pixi.js v8 的事件系统支持鼠标、触摸、指针事件,具备事件冒泡、自定义碰撞区域等功能。同时,键盘和手柄输入需要通过浏览器原生 API 接入。
EventMode 交互模式
在 Pixi.js v8 中,必须将 eventMode 设置为可交互状态才能接收事件:
| 值 | 说明 |
|---|---|
'none' | 不响应任何事件(默认) |
'passive' | 不响应事件,但允许事件冒泡经过 |
'auto' | 仅在可交互父容器内响应事件 |
'static' | 响应事件,但不触发 move 相关事件(推荐按钮等) |
'dynamic' | 响应所有事件,包括 move(推荐拖拽等) |
import { Application, Sprite, Graphics, Assets } from 'pixi.js';
const app = new Application();
await app.init({ width: 800, height: 600, background: '#1a1a2e' });
document.body.appendChild(app.canvas);
const texture = await Assets.load('/image/button.png');
const button = new Sprite(texture);
button.position.set(350, 250);
button.eventMode = 'static'; // 启用事件
button.cursor = 'pointer'; // 鼠标样式
button.on('pointerdown', () => {
console.log('按钮被点击!');
});
app.stage.addChild(button);
💡 提示: 'static' 适用于按钮等不需要追踪鼠标移动的对象;'dynamic' 适用于需要 pointermove 的拖拽对象。
鼠标/指针事件
事件类型
| 事件名 | 触发时机 |
|---|---|
pointerdown | 指针按下 |
pointerup | 指针抬起 |
pointerupoutside | 指针在对象外抬起 |
pointermove | 指针移动 |
pointerover | 指针进入对象 |
pointerout | 指针离开对象 |
pointertap | 指针点击(按下+抬起在同一对象) |
click | 同 pointertap |
mousedown | 鼠标按下 |
mouseup | 鼠标抬起 |
mousemove | 鼠标移动 |
mouseover | 鼠标进入 |
mouseout | 鼠标离开 |
事件对象
sprite.on('pointerdown', (event) => {
// 事件类型
console.log(event.type); // 'pointerdown'
// 指针位置
console.log(event.global.x); // 画布坐标
console.log(event.global.y);
// 相对于目标对象的局部坐标
const local = sprite.toLocal(event.global);
console.log(local.x, local.y);
// 按钮信息(左键=0, 中键=1, 右键=2)
console.log(event.button);
// 修饰键
console.log(event.ctrlKey);
console.log(event.shiftKey);
console.log(event.altKey);
// 原始 DOM 事件
console.log(event.nativeEvent);
});
完整交互示例
const interactiveRect = new Graphics();
interactiveRect.rect(0, 0, 200, 150);
interactiveRect.fill(0x3498db);
interactiveRect.position.set(300, 200);
interactiveRect.eventMode = 'static';
interactiveRect.cursor = 'pointer';
// 悬停变色
interactiveRect.on('pointerover', () => {
interactiveRect.tint = 0x5dade2;
});
interactiveRect.on('pointerout', () => {
interactiveRect.tint = 0xffffff;
});
// 点击反馈
interactiveRect.on('pointerdown', () => {
interactiveRect.scale.set(0.95);
});
interactiveRect.on('pointerup', () => {
interactiveRect.scale.set(1.0);
});
interactiveRect.on('pointerupoutside', () => {
interactiveRect.scale.set(1.0);
});
app.stage.addChild(interactiveRect);
触摸事件
Pixi.js 的指针事件已统一处理鼠标和触摸,通常无需单独处理触摸:
sprite.on('pointerdown', (e) => {
// 始终触发,无论是鼠标还是触摸
console.log('pointer type:', e.pointerType); // 'mouse' | 'touch' | 'pen'
});
// 仅针对触摸设备
sprite.on('touchstart', (e) => {
console.log('触摸开始');
});
⚠️ 注意: 移动端浏览器默认有 300ms 点击延迟和触摸滚动行为。可通过 CSS 禁用:
canvas {
touch-action: none;
}
事件冒泡
Pixi.js 事件支持沿场景树向上冒泡:
const parent = new Container();
parent.eventMode = 'static';
parent.hitArea = { x: 0, y: 0, width: 800, height: 600 };
const child = new Sprite(texture);
child.eventMode = 'static';
parent.addChild(child);
// 子对象事件冒泡到父对象
parent.on('pointerdown', () => {
console.log('父容器被点击(可能是冒泡)');
});
child.on('pointerdown', (e) => {
console.log('子精灵被点击');
// e.stopPropagation(); // 阻止冒泡
});
hitArea 碰撞区域
默认情况下,只有像素非透明的区域才可交互。可通过 hitArea 自定义碰撞区域:
import { Rectangle, Circle, Ellipse, Polygon, RoundedRectangle } from 'pixi.js';
// 矩形碰撞区
sprite.hitArea = new Rectangle(0, 0, 100, 100);
// 圆形碰撞区
sprite.hitArea = new Circle(50, 50, 50);
// 多边形碰撞区
sprite.hitArea = new Polygon([0, 0, 100, 0, 100, 100, 0, 100]);
// 圆角矩形
sprite.hitArea = new RoundedRectangle(0, 0, 100, 100, 16);
// 椭圆
sprite.hitArea = new Ellipse(50, 50, 50, 30);
// 自定义检测函数
sprite.hitArea = {
contains(x: number, y: number): boolean {
// 自定义碰撞逻辑
return x > 0 && x < 100 && y > 0 && y < 100;
},
};
拖拽实现
class DragHandler {
private target: Sprite;
private dragging: boolean = false;
private dragOffset = { x: 0, y: 0 };
constructor(target: Sprite) {
this.target = target;
this.target.eventMode = 'static';
this.target.cursor = 'grab';
this.target.on('pointerdown', this.onDragStart.bind(this));
this.target.on('pointermove', this.onDragMove.bind(this));
this.target.on('pointerup', this.onDragEnd.bind(this));
this.target.on('pointerupoutside', this.onDragEnd.bind(this));
}
private onDragStart(e: FederatedPointerEvent) {
this.dragging = true;
this.target.cursor = 'grabbing';
const pos = e.global;
this.dragOffset.x = this.target.x - pos.x;
this.dragOffset.y = this.target.y - pos.y;
this.target.alpha = 0.8;
}
private onDragMove(e: FederatedPointerEvent) {
if (!this.dragging) return;
const pos = e.global;
this.target.x = pos.x + this.dragOffset.x;
this.target.y = pos.y + this.dragOffset.y;
}
private onDragEnd() {
this.dragging = false;
this.target.cursor = 'grab';
this.target.alpha = 1;
}
}
// 使用
const draggableSprite = new Sprite(texture);
draggableSprite.position.set(300, 200);
new DragHandler(draggableSprite);
app.stage.addChild(draggableSprite);
💡 提示: 对于需要 pointermove 事件的对象,必须设置 eventMode = 'dynamic',而非 'static'。
键盘输入
键盘事件不在 Pixi.js 事件系统中,直接使用浏览器 API:
class KeyboardInput {
private keys = new Set<string>();
private justPressed = new Set<string>();
constructor() {
document.addEventListener('keydown', (e) => {
if (!this.keys.has(e.code)) {
this.justPressed.add(e.code);
}
this.keys.add(e.code);
});
document.addEventListener('keyup', (e) => {
this.keys.delete(e.code);
});
}
isDown(code: string): boolean {
return this.keys.has(code);
}
wasPressed(code: string): boolean {
return this.justPressed.has(code);
}
clear() {
this.justPressed.clear();
}
}
// 使用
const input = new KeyboardInput();
app.ticker.add(() => {
if (input.isDown('ArrowLeft')) player.x -= 3;
if (input.isDown('ArrowRight')) player.x += 3;
if (input.isDown('ArrowUp')) player.y -= 3;
if (input.isDown('ArrowDown')) player.y += 3;
if (input.wasPressed('Space')) shootBullet();
input.clear(); // 清除 justPressed 状态
});
⚠️ 注意: 需要先在画布上点击使其获得焦点,键盘事件才会触发。或者使用 document.addEventListener 全局监听。
手柄输入(Gamepad API)
class GamepadInput {
update() {
const gamepads = navigator.getGamepads();
for (const gp of gamepads) {
if (!gp) continue;
// 左摇杆: axes[0](X), axes[1](Y)
// A按钮: buttons[0].pressed
console.log('摇杆X:', gp.axes[0], 'A键:', gp.buttons[0].pressed);
}
}
}
💡 提示: 手柄输入需要用户先按下任意按钮才能检测到连接。Gamepad API 通过 navigator.getGamepads() 轮询状态,建议在 ticker 中调用。
自定义事件系统
import { EventEmitter } from 'pixi.js';
const gameEvents = new EventEmitter();
// 监听
gameEvents.on('enemy:killed', (data: { id: number; score: number }) => {
score += data.score;
scoreText.text = `Score: ${score}`;
});
// 发送
gameEvents.emit('enemy:killed', { id: 42, score: 100 });
输入管理器封装
将键盘、鼠标、手柄统一管理,提供简洁接口:
class InputManager {
private keyboard: KeyboardInput;
private mouse = { x: 0, y: 0, buttons: new Set<number>() };
constructor(app: Application) {
this.keyboard = new KeyboardInput();
app.stage.eventMode = 'static';
app.stage.hitArea = app.screen;
app.stage.on('pointermove', (e) => {
this.mouse.x = e.global.x;
this.mouse.y = e.global.y;
});
app.stage.on('pointerdown', (e) => this.mouse.buttons.add(e.button));
app.stage.on('pointerup', (e) => this.mouse.buttons.delete(e.button));
}
get kb() { return this.keyboard; }
get mousePos() { return this.mouse; }
update() { this.keyboard.clear(); }
}
💡 提示: 统一的输入管理器使游戏逻辑与具体输入设备解耦,方便支持键盘、手柄、触摸等多种输入方式。
游戏开发场景
场景:按钮组件
class Button extends Container {
private bg: Graphics;
private label: Text;
constructor(text: string, width: number, height: number, color: number) {
super();
this.bg = new Graphics();
this.bg.roundRect(0, 0, width, height, 8);
this.bg.fill(color);
this.addChild(this.bg);
this.label = new Text({
text,
style: { fontSize: 20, fill: '#ffffff', fontWeight: 'bold' },
});
this.label.anchor.set(0.5);
this.label.position.set(width / 2, height / 2);
this.addChild(this.label);
this.eventMode = 'static';
this.cursor = 'pointer';
this.hitArea = new Rectangle(0, 0, width, height);
this.on('pointerover', () => this.scale.set(1.05));
this.on('pointerout', () => this.scale.set(1.0));
this.on('pointerdown', () => this.scale.set(0.95));
this.on('pointerup', () => this.scale.set(1.05));
}
}
// 使用
const startBtn = new Button('开始游戏', 200, 50, 0x27ae60);
startBtn.position.set(300, 350);
startBtn.on('pointertap', () => startGame());
app.stage.addChild(startBtn);
⚠️ 常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 点击无响应 | eventMode 未设置 | 设置 eventMode = 'static' |
| pointermove 不触发 | eventMode 为 ‘static’ | 改为 'dynamic' |
| 移动端点击延迟 | 浏览器默认行为 | CSS touch-action: none |
| 事件触发在错误位置 | stage 有缩放/偏移 | 使用 toLocal 转换坐标 |
| 键盘事件不触发 | canvas 未获得焦点 | canvas 设 tabindex 或用 document |
| 多点触控不支持 | 未处理 touch 数组 | 使用 event.getCoalescedEvents() |
💡 进阶提示
防误触: 按钮添加防抖:
let lastClick = 0; button.on('pointertap', () => { if (Date.now() - lastClick < 300) return; lastClick = Date.now(); handleClick(); });触摸多点支持:
app.stage.on('pointerdown', (e) => { const events = e.getCoalescedEvents(); for (const evt of events) { console.log('触点:', evt.pointerId, evt.global); } });鼠标坐标调试:
const mouseDebug = new Text({ text: '', style: { fontSize: 12, fill: '#0f0' } }); mouseDebug.position.set(10, 10); app.stage.addChild(mouseDebug); app.stage.on('pointermove', (e) => { mouseDebug.text = `Mouse: ${Math.round(e.global.x)}, ${Math.round(e.global.y)}`; });