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