PixiJS 交互事件 #
事件系统概述 #
PixiJS 提供了完善的事件系统,支持鼠标、触摸和键盘等输入设备的交互。
text
┌─────────────────────────────────────────────────────────────┐
│ 事件系统架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 事件源 │
│ ├── 鼠标事件 │
│ ├── 触摸事件 │
│ ├── 键盘事件 │
│ └── 游戏手柄 │
│ │
│ 事件处理 │
│ ├── 事件捕获 │
│ ├── 事件冒泡 │
│ └── 事件委托 │
│ │
│ 交互模式 │
│ ├── static(静态交互) │
│ ├── dynamic(动态交互) │
│ └── none(无交互) │
│ │
└─────────────────────────────────────────────────────────────┘
事件模式 #
设置交互模式 #
javascript
import { Sprite } from 'pixi.js';
const sprite = Sprite.from('button.png');
// 启用交互
sprite.eventMode = 'static';
// 或使用动态模式(更高效,适合频繁交互)
sprite.eventMode = 'dynamic';
// 禁用交互
sprite.eventMode = 'none';
事件模式对比 #
| 模式 | 特点 | 适用场景 |
|---|---|---|
| none | 不响应任何事件 | 静态背景元素 |
| static | 响应事件,无 hitArea 时使用边界检测 | 按钮、静态 UI |
| dynamic | 响应事件,支持 hitArea 和自定义检测 | 复杂形状、频繁交互 |
鼠标事件 #
基本鼠标事件 #
javascript
const sprite = Sprite.from('button.png');
sprite.eventMode = 'static';
sprite.anchor.set(0.5);
// 点击
sprite.on('pointerdown', (event) => {
console.log('按下', event.global);
});
// 释放
sprite.on('pointerup', (event) => {
console.log('释放');
});
// 点击完成(按下并释放)
sprite.on('click', (event) => {
console.log('点击');
});
// 右键点击
sprite.on('rightclick', (event) => {
console.log('右键点击');
});
// 双击
sprite.on('dblclick', (event) => {
console.log('双击');
});
悬停事件 #
javascript
const sprite = Sprite.from('button.png');
sprite.eventMode = 'static';
// 鼠标进入
sprite.on('pointerover', (event) => {
sprite.alpha = 0.8;
sprite.cursor = 'pointer';
});
// 鼠标离开
sprite.on('pointerout', (event) => {
sprite.alpha = 1;
});
// 鼠标移动(在元素上)
sprite.on('pointermove', (event) => {
console.log('鼠标位置:', event.global);
});
鼠标按钮 #
javascript
sprite.on('pointerdown', (event) => {
// event.button 表示按下的按钮
// 0: 左键
// 1: 中键
// 2: 右键
switch (event.button) {
case 0:
console.log('左键按下');
break;
case 1:
console.log('中键按下');
break;
case 2:
console.log('右键按下');
break;
}
});
修饰键 #
javascript
sprite.on('click', (event) => {
// 检查修饰键
if (event.ctrlKey) {
console.log('Ctrl + 点击');
}
if (event.shiftKey) {
console.log('Shift + 点击');
}
if (event.altKey) {
console.log('Alt + 点击');
}
if (event.metaKey) {
console.log('Meta + 点击');
}
});
触摸事件 #
触摸事件处理 #
javascript
const sprite = Sprite.from('button.png');
sprite.eventMode = 'static';
// 触摸开始
sprite.on('pointerdown', (event) => {
console.log('触摸开始');
console.log('触摸点数量:', event.pointerType);
});
// 触摸移动
sprite.on('pointermove', (event) => {
console.log('触摸位置:', event.global);
});
// 触摸结束
sprite.on('pointerup', (event) => {
console.log('触摸结束');
});
// 触摸取消
sprite.on('pointercancel', (event) => {
console.log('触摸取消');
});
多点触控 #
javascript
const container = new Container();
container.eventMode = 'static';
container.hitArea = app.screen;
const touches = new Map();
container.on('pointerdown', (event) => {
const touchId = event.pointerId;
const touchPoint = new Graphics();
touchPoint.circle(0, 0, 30);
touchPoint.fill({ color: 0xff0000 });
touchPoint.position.copyFrom(event.global);
container.addChild(touchPoint);
touches.set(touchId, touchPoint);
});
container.on('pointermove', (event) => {
const touchPoint = touches.get(event.pointerId);
if (touchPoint) {
touchPoint.position.copyFrom(event.global);
}
});
container.on('pointerup', (event) => {
const touchPoint = touches.get(event.pointerId);
if (touchPoint) {
container.removeChild(touchPoint);
touches.delete(event.pointerId);
}
});
键盘事件 #
键盘监听 #
javascript
// 监听键盘事件
app.stage.eventMode = 'static';
app.stage.on('keydown', (event) => {
console.log('按键:', event.key);
console.log('键码:', event.code);
});
app.stage.on('keyup', (event) => {
console.log('释放:', event.key);
});
键盘控制 #
javascript
const player = Sprite.from('player.png');
player.x = 400;
player.y = 300;
const keys = {
up: false,
down: false,
left: false,
right: false
};
// 键盘按下
window.addEventListener('keydown', (e) => {
switch (e.code) {
case 'ArrowUp':
case 'KeyW':
keys.up = true;
break;
case 'ArrowDown':
case 'KeyS':
keys.down = true;
break;
case 'ArrowLeft':
case 'KeyA':
keys.left = true;
break;
case 'ArrowRight':
case 'KeyD':
keys.right = true;
break;
}
});
// 键盘释放
window.addEventListener('keyup', (e) => {
switch (e.code) {
case 'ArrowUp':
case 'KeyW':
keys.up = false;
break;
case 'ArrowDown':
case 'KeyS':
keys.down = false;
break;
case 'ArrowLeft':
case 'KeyA':
keys.left = false;
break;
case 'ArrowRight':
case 'KeyD':
keys.right = false;
break;
}
});
// 游戏循环中处理移动
app.ticker.add(() => {
const speed = 5;
if (keys.up) player.y -= speed;
if (keys.down) player.y += speed;
if (keys.left) player.x -= speed;
if (keys.right) player.x += speed;
});
键盘管理器 #
javascript
class KeyboardManager {
constructor() {
this.keys = new Map();
window.addEventListener('keydown', (e) => {
this.keys.set(e.code, true);
});
window.addEventListener('keyup', (e) => {
this.keys.set(e.code, false);
});
}
isDown(keyCode) {
return this.keys.get(keyCode) === true;
}
isUp(keyCode) {
return !this.isDown(keyCode);
}
getAxis(negative, positive) {
let value = 0;
if (this.isDown(negative)) value -= 1;
if (this.isDown(positive)) value += 1;
return value;
}
}
const keyboard = new KeyboardManager();
// 使用
app.ticker.add(() => {
const horizontal = keyboard.getAxis('ArrowLeft', 'ArrowRight');
const vertical = keyboard.getAxis('ArrowUp', 'ArrowDown');
player.x += horizontal * speed;
player.y += vertical * speed;
});
拖拽功能 #
基本拖拽 #
javascript
const sprite = Sprite.from('draggable.png');
sprite.eventMode = 'static';
sprite.anchor.set(0.5);
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
sprite.on('pointerdown', (event) => {
isDragging = true;
dragOffset.x = event.global.x - sprite.x;
dragOffset.y = event.global.y - sprite.y;
sprite.cursor = 'grabbing';
});
sprite.on('pointermove', (event) => {
if (isDragging) {
sprite.x = event.global.x - dragOffset.x;
sprite.y = event.global.y - dragOffset.y;
}
});
sprite.on('pointerup', () => {
isDragging = false;
sprite.cursor = 'grab';
});
sprite.on('pointerupoutside', () => {
isDragging = false;
sprite.cursor = 'grab';
});
拖拽管理器 #
javascript
class Draggable {
constructor(displayObject, options = {}) {
this.displayObject = displayObject;
this.options = {
bounds: options.bounds || null,
onDragStart: options.onDragStart || null,
onDrag: options.onDrag || null,
onDragEnd: options.onDragEnd || null
};
this.isDragging = false;
this.dragOffset = { x: 0, y: 0 };
this.setupEvents();
}
setupEvents() {
const obj = this.displayObject;
obj.eventMode = 'static';
obj.cursor = 'grab';
obj.on('pointerdown', this.onDragStart, this);
obj.on('pointermove', this.onDragMove, this);
obj.on('pointerup', this.onDragEnd, this);
obj.on('pointerupoutside', this.onDragEnd, this);
}
onDragStart(event) {
this.isDragging = true;
this.dragOffset.x = event.global.x - this.displayObject.x;
this.dragOffset.y = event.global.y - this.displayObject.y;
this.displayObject.cursor = 'grabbing';
if (this.options.onDragStart) {
this.options.onDragStart(event);
}
}
onDragMove(event) {
if (!this.isDragging) return;
let x = event.global.x - this.dragOffset.x;
let y = event.global.y - this.dragOffset.y;
// 边界限制
if (this.options.bounds) {
const bounds = this.options.bounds;
x = Math.max(bounds.x, Math.min(bounds.x + bounds.width, x));
y = Math.max(bounds.y, Math.min(bounds.y + bounds.height, y));
}
this.displayObject.x = x;
this.displayObject.y = y;
if (this.options.onDrag) {
this.options.onDrag(event);
}
}
onDragEnd(event) {
this.isDragging = false;
this.displayObject.cursor = 'grab';
if (this.options.onDragEnd) {
this.options.onDragEnd(event);
}
}
}
// 使用
const sprite = Sprite.from('item.png');
app.stage.addChild(sprite);
const draggable = new Draggable(sprite, {
bounds: { x: 0, y: 0, width: 800, height: 600 },
onDragStart: () => console.log('开始拖拽'),
onDragEnd: () => console.log('结束拖拽')
});
点击检测 #
Hit Area(点击区域) #
javascript
const sprite = Sprite.from('button.png');
sprite.eventMode = 'static';
// 使用矩形点击区域
sprite.hitArea = new Rectangle(0, 0, 100, 50);
// 使用圆形点击区域
sprite.hitArea = new Circle(50, 25, 30);
// 使用多边形点击区域
sprite.hitArea = new Polygon([0, 0, 100, 0, 50, 100]);
// 使用自定义检测函数
sprite.hitArea = {
contains(x, y) {
// 自定义检测逻辑
return x > 0 && x < 100 && y > 0 && y < 50;
}
};
自定义命中测试 #
javascript
class InteractiveGraphics extends Graphics {
constructor() {
super();
this.eventMode = 'static';
this.containsPoint = this.customHitTest;
}
customHitTest(point) {
// 自定义命中测试逻辑
const localPoint = this.toLocal(point);
// 检查是否在特定区域内
return localPoint.x > 0 && localPoint.x < 100 &&
localPoint.y > 0 && localPoint.y < 100;
}
}
事件冒泡 #
事件传播 #
text
┌─────────────────────────────────────────────────────────────┐
│ 事件传播流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 捕获阶段(从外到内) │
│ Stage ─── Container ─── Sprite │
│ │
│ 目标阶段 │
│ Sprite(触发事件的对象) │
│ │
│ 冒泡阶段(从内到外) │
│ Sprite ─── Container ─── Stage │
│ │
└─────────────────────────────────────────────────────────────┘
控制事件传播 #
javascript
const container = new Container();
const sprite = Sprite.from('item.png');
container.eventMode = 'static';
sprite.eventMode = 'static';
container.addChild(sprite);
// 容器事件
container.on('pointerdown', (event) => {
console.log('容器点击');
});
// 精灵事件
sprite.on('pointerdown', (event) => {
console.log('精灵点击');
// 阻止事件冒泡
event.stopPropagation();
});
手势识别 #
点击手势 #
javascript
class TapGesture {
constructor(displayObject, callback, options = {}) {
this.target = displayObject;
this.callback = callback;
this.maxDuration = options.maxDuration || 300;
this.maxDistance = options.maxDistance || 10;
this.startTime = 0;
this.startPos = { x: 0, y: 0 };
displayObject.eventMode = 'static';
displayObject.on('pointerdown', this.onDown, this);
displayObject.on('pointerup', this.onUp, this);
}
onDown(event) {
this.startTime = Date.now();
this.startPos = { ...event.global };
}
onUp(event) {
const duration = Date.now() - this.startTime;
const distance = Math.hypot(
event.global.x - this.startPos.x,
event.global.y - this.startPos.y
);
if (duration < this.maxDuration && distance < this.maxDistance) {
this.callback(event);
}
}
}
长按手势 #
javascript
class LongPressGesture {
constructor(displayObject, callback, options = {}) {
this.target = displayObject;
this.callback = callback;
this.minDuration = options.minDuration || 500;
this.timer = null;
displayObject.eventMode = 'static';
displayObject.on('pointerdown', this.onDown, this);
displayObject.on('pointerup', this.onUp, this);
displayObject.on('pointerupoutside', this.onUp, this);
}
onDown(event) {
this.timer = setTimeout(() => {
this.callback(event);
}, this.minDuration);
}
onUp() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
}
滑动手势 #
javascript
class SwipeGesture {
constructor(displayObject, callback, options = {}) {
this.target = displayObject;
this.callback = callback;
this.minDistance = options.minDistance || 50;
this.maxDuration = options.maxDuration || 500;
this.startTime = 0;
this.startPos = { x: 0, y: 0 };
displayObject.eventMode = 'static';
displayObject.on('pointerdown', this.onDown, this);
displayObject.on('pointerup', this.onUp, this);
}
onDown(event) {
this.startTime = Date.now();
this.startPos = { ...event.global };
}
onUp(event) {
const duration = Date.now() - this.startTime;
if (duration > this.maxDuration) return;
const dx = event.global.x - this.startPos.x;
const dy = event.global.y - this.startPos.y;
const distance = Math.hypot(dx, dy);
if (distance < this.minDistance) return;
let direction;
if (Math.abs(dx) > Math.abs(dy)) {
direction = dx > 0 ? 'right' : 'left';
} else {
direction = dy > 0 ? 'down' : 'up';
}
this.callback(direction, { dx, dy, distance });
}
}
双指缩放 #
javascript
class PinchGesture {
constructor(displayObject, callback) {
this.target = displayObject;
this.callback = callback;
this.touches = new Map();
this.initialDistance = 0;
this.initialScale = 1;
displayObject.eventMode = 'static';
displayObject.hitArea = app.screen;
displayObject.on('pointerdown', this.onDown, this);
displayObject.on('pointermove', this.onMove, this);
displayObject.on('pointerup', this.onUp, this);
}
onDown(event) {
this.touches.set(event.pointerId, { ...event.global });
if (this.touches.size === 2) {
this.initialDistance = this.getDistance();
this.initialScale = this.target.scale.x;
}
}
onMove(event) {
if (this.touches.has(event.pointerId)) {
this.touches.set(event.pointerId, { ...event.global });
}
if (this.touches.size === 2) {
const currentDistance = this.getDistance();
const scale = this.initialScale * (currentDistance / this.initialDistance);
this.callback(scale);
}
}
onUp(event) {
this.touches.delete(event.pointerId);
}
getDistance() {
const points = Array.from(this.touches.values());
return Math.hypot(
points[0].x - points[1].x,
points[0].y - points[1].y
);
}
}
实战示例 #
可点击按钮 #
javascript
class Button extends Container {
constructor(text, width = 150, height = 50) {
super();
this.buttonWidth = width;
this.buttonHeight = height;
this.background = new Graphics();
this.label = new Text({
text,
style: {
fontFamily: 'Arial',
fontSize: 18,
fill: 0xffffff
}
});
this.draw();
this.label.anchor.set(0.5);
this.label.x = width / 2;
this.label.y = height / 2;
this.addChild(this.background, this.label);
this.eventMode = 'static';
this.cursor = 'pointer';
this.on('pointerover', this.onOver, this);
this.on('pointerout', this.onOut, this);
this.on('pointerdown', this.onDown, this);
this.on('pointerup', this.onUp, this);
}
draw(isHovered = false, isPressed = false) {
this.background.clear();
let color = 0x3366cc;
if (isPressed) color = 0x224488;
else if (isHovered) color = 0x4488ff;
this.background.roundRect(0, 0, this.buttonWidth, this.buttonHeight, 8);
this.background.fill({ color });
}
onOver() {
this.draw(true);
}
onOut() {
this.draw();
}
onDown() {
this.draw(false, true);
}
onUp() {
this.draw(true);
this.emit('click');
}
}
const button = new Button('Click Me');
button.on('click', () => console.log('按钮被点击'));
app.stage.addChild(button);
拖拽排序 #
javascript
class DragSortList extends Container {
constructor(items) {
super();
this.items = [];
this.itemHeight = 60;
this.draggingItem = null;
this.dragIndex = -1;
items.forEach((text, i) => {
this.addItem(text, i);
});
}
addItem(text, index) {
const item = new Container();
const bg = new Graphics();
bg.roundRect(0, 0, 300, 50, 5);
bg.fill({ color: 0x333333 });
const label = new Text({
text,
style: { fontFamily: 'Arial', fontSize: 18, fill: 0xffffff }
});
label.x = 15;
label.y = 15;
item.addChild(bg, label);
item.y = index * this.itemHeight;
item.originalY = item.y;
item.eventMode = 'static';
item.cursor = 'grab';
item.on('pointerdown', this.onItemDown, this);
item.on('pointermove', this.onItemMove, this);
item.on('pointerup', this.onItemUp, this);
this.items.push(item);
this.addChild(item);
}
onItemDown(event) {
this.draggingItem = event.currentTarget;
this.dragIndex = this.items.indexOf(this.draggingItem);
this.draggingItem.cursor = 'grabbing';
this.addChild(this.draggingItem);
}
onItemMove(event) {
if (!this.draggingItem) return;
this.draggingItem.y = event.global.y - 25;
// 计算新位置
const newIndex = Math.round(this.draggingItem.y / this.itemHeight);
const clampedIndex = Math.max(0, Math.min(this.items.length - 1, newIndex));
if (clampedIndex !== this.dragIndex) {
// 重新排序
this.items.splice(this.dragIndex, 1);
this.items.splice(clampedIndex, 0, this.draggingItem);
this.dragIndex = clampedIndex;
// 更新所有项位置
this.items.forEach((item, i) => {
if (item !== this.draggingItem) {
item.y = i * this.itemHeight;
}
});
}
}
onItemUp() {
if (this.draggingItem) {
this.draggingItem.y = this.dragIndex * this.itemHeight;
this.draggingItem.cursor = 'grab';
this.draggingItem = null;
}
}
}
下一步 #
掌握了交互事件后,接下来学习 动画系统,了解如何创建流畅的动画效果!
最后更新:2026-03-29