mcpField
S
starwards-pixijs

starwards-pixijs

github.com/starwards开发4447 次安装2 天前更新
代码仓库

关于 starwards-pixijs

这是面向 Starwards 浏览器端开发的 PixiJS v8 参考与实践 skill,适合使用 PixiJS 构建 2D 渲染功能的开发者。内容结合 API 用法与 Starwards 的分层、事件和对象池模式。

主要功能

  1. 01PixiJS v8 核心 API 参考
  2. 02容器与场景图组织
  3. 03资源、纹理和图形绘制
  4. 04Ticker 驱动更新与交互
  5. 05Starwards 分层及对象池模式

适用场景

  1. 01实现浏览器端 2D 渲染
  2. 02将 PixiJS v7 代码迁移至 v8
  3. 03开发 Starwards 雷达或战术视图

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/starwards/starwards --skill starwards-pixijs

安装后在 Claude Code 中输入/starwards-pixijs确认已加载。

详细介绍

Starwards 的 PixiJS v8 开发

概述

Starwards 在浏览器模块中使用 PixiJS v8(^8.14.0)进行 2D 渲染。本技能涵盖 PixiJS v8 API 参考和 Starwards 特有模式。

核心原则: 采用分层容器组合,并通过 ticker 驱动更新,使其与 Colyseus 状态变更同步。

目录

  1. PixiJS v8 参考
  2. Starwards 模式

PixiJS v8 参考

Application

Application 类为 PixiJS 项目提供可扩展的入口点。

异步初始化(v8 必需)
import { Application } from 'pixi.js';

const app = new Application();

await app.init({
  width: 800,
  height: 600,
  backgroundColor: 0x1099bb,
});

document.body.appendChild(app.canvas);
关键选项
选项类型默认值描述
widthnumber800初始宽度
heightnumber600初始高度
backgroundColorColorSource'black'背景颜色
antialiasboolean-启用抗锯齿
resolutionnumber1像素分辨率
resizeToWindow | HTMLElement-自动调整尺寸的目标
preference'webgl' | 'webgpu''webgl'渲染器类型

容器与场景图

创建容器
import { Container } from 'pixi.js';

const container = new Container({
  x: 100,
  y: 100,
});

app.stage.addChild(container);
父子关系
  • 子项会继承父级的变换、透明度和可见性
  • 渲染顺序:子项按插入顺序渲染(越晚插入越靠上)
  • 使用 setChildIndex(),或搭配 sortableChildren 使用 zIndex 进行重新排序
坐标系
// Local to global
const globalPos = obj.toGlobal(new Point(0, 0));

// Global to local
const localPos = container.toLocal(new Point(100, 100));
剔除
container.cullable = true;        // Enable culling
container.cullableChildren = true; // Cull children recursively
container.cullArea = new Rectangle(0, 0, 400, 400); // Custom cull bounds

精灵

基本用法
import { Sprite, Assets } from 'pixi.js';

const texture = await Assets.load('bunny.png');
const sprite = new Sprite(texture);

sprite.anchor.set(0.5);  // Center anchor
sprite.x = 100;
sprite.y = 100;
sprite.tint = 0xff0000;  // Red tint
sprite.alpha = 0.8;
精灵属性
属性描述
texture要显示的纹理
anchor原点位置(范围为 0-1)
tint色调
blendMode合成混合模式
width、height尺寸(会缩放纹理)

图形(v8 API)

重要: v8 使用全新的流式 API。先构建形状,再进行填充或描边。

v8 流式 API
import { Graphics } from 'pixi.js';

// Draw shape, then fill/stroke
const graphics = new Graphics()
  .rect(50, 50, 100, 100)
  .fill(0xff0000)
  .stroke({ width: 2, color: 'white' });

// Circle with fill and stroke
const circle = new Graphics()
  .circle(100, 100, 50)
  .fill({ color: 0x00ff00, alpha: 0.5 })
  .stroke({ width: 3, color: 0x000000 });
形状方法
v7(旧)v8(新)
drawRect()rect()
drawCircle()circle()
drawEllipse()ellipse()
drawRoundedRect()roundRect()
drawPolygon()poly()
drawStar()star()
线条
const lines = new Graphics()
  .moveTo(0, 0)
  .lineTo(100, 100)
  .lineTo(200, 0)
  .stroke({ width: 2, color: 0xff0000 });
孔洞(v8)
const rectWithHole = new Graphics()
  .rect(0, 0, 100, 100)
  .fill(0x00ff00)
  .circle(50, 50, 20)
  .cut();  // Creates hole
GraphicsContext(共享)
import { GraphicsContext, Graphics } from 'pixi.js';

const context = new GraphicsContext()
  .rect(0, 0, 100, 100)
  .fill(0xff0000);

const g1 = new Graphics(context);
const g2 = new Graphics(context); // Shares same data

文本

基本文本
import { Text, TextStyle } from 'pixi.js';

const text = new Text({
  text: 'Hello World',
  style: {
    fontFamily: 'Arial',
    fontSize: 24,
    fill: 0xffffff,
    align: 'center',
  },
});
TextStyle 属性
属性描述
fontFamily字体名称
fontSize字号(像素)
fill填充颜色
stroke描边设置
align文本对齐方式
wordWrap启用自动换行
wordWrapWidth换行宽度
BitmapText(性能)
import { BitmapText } from 'pixi.js';

const bitmapText = new BitmapText({
  text: 'Score: 1000',
  style: { fontFamily: 'MyBitmapFont', fontSize: 32 },
});

纹理与资源

加载资源
import { Assets, Sprite } from 'pixi.js';

// Single asset
const texture = await Assets.load('path/to/image.png');
const sprite = new Sprite(texture);

// Multiple assets
const textures = await Assets.load(['a.png', 'b.png']);

// With alias
await Assets.load({ alias: 'hero', src: 'images/hero.png' });
const heroTexture = Assets.get('hero');
资源包
Assets.addBundle('game', [
  { alias: 'player', src: 'player.png' },
  { alias: 'enemy', src: 'enemy.png' },
]);

const assets = await Assets.loadBundle('game');
清单
const manifest = {
  bundles: [
    {
      name: 'load-screen',
      assets: [{ alias: 'bg', src: 'background.png' }],
    },
    {
      name: 'game',
      assets: [{ alias: 'hero', src: 'hero.png' }],
    },
  ],
};

await Assets.init({ manifest });
await Assets.loadBundle('load-screen');
SVG
// As texture
const svgTexture = await Assets.load('icon.svg');
const sprite = new Sprite(svgTexture);

// As Graphics (scalable)
const svgContext = await Assets.load({
  src: 'icon.svg',
  data: { parseAsGraphicsContext: true },
});
const graphics = new Graphics(svgContext);
纹理清理
// Unload from cache and GPU
await Assets.unload('texture.png');

// Unload from GPU only (keep in memory)
texture.source.unload();

// Destroy texture
texture.destroy();

Ticker

基本用法
import { Ticker, UPDATE_PRIORITY } from 'pixi.js';

// Using app ticker
app.ticker.add((ticker) => {
  sprite.rotation += 0.1 * ticker.deltaTime;
});

// One-time callback
app.ticker.addOnce((ticker) => {
  console.log('Called once');
});

// With priority (higher runs first)
app.ticker.add(updateFn, null, UPDATE_PRIORITY.HIGH);
优先级常量
  • UPDATE_PRIORITY.HIGH = 50
  • UPDATE_PRIORITY.NORMAL = 0
  • UPDATE_PRIORITY.LOW = -50
FPS 控制
app.ticker.maxFPS = 60;  // Cap framerate
app.ticker.minFPS = 30;  // Clamp deltaTime
Ticker 属性
属性描述
deltaTime缩放后的帧间隔
elapsedMS距上一帧经过的实际毫秒数
FPS当前每秒帧数

事件 / 交互

事件模式
sprite.eventMode = 'static';  // Interactive, non-moving
sprite.eventMode = 'dynamic'; // Interactive, moving (receives idle events)
sprite.eventMode = 'passive'; // Default, children can be interactive
sprite.eventMode = 'none';    // No interaction
指针事件
sprite.eventMode = 'static';

sprite.on('pointerdown', (event) => {
  console.log('Clicked at', event.global.x, event.global.y);
});

sprite.on('pointermove', (event) => { /* ... */ });
sprite.on('pointerup', (event) => { /* ... */ });
sprite.on('pointerover', (event) => { /* ... */ });
sprite.on('pointerout', (event) => { /* ... */ });
点击区域
import { Rectangle, Circle } from 'pixi.js';

sprite.hitArea = new Rectangle(0, 0, 100, 100);
// or
sprite.hitArea = new Circle(50, 50, 50);
自定义光标
sprite.cursor = 'pointer';
sprite.cursor = 'grab';
sprite.cursor = 'url(cursor.png), auto';
禁用子项交互
container.interactiveChildren = false; // Skip children hit testing

性能提示

精灵图
  • 使用精灵图集以尽量减少纹理切换
  • 每个批次最多可将 16 个纹理的精灵合批
  • 绘制顺序会影响合批效率
图形
  • 创建后不再修改的图形性能最快
  • 小型图形(<100 个点)可以像精灵一样合批
  • 复杂形状使用带纹理的精灵
文本
  • 避免每帧更新文本(开销较大)
  • 对于频繁变化的文本,使用 BitmapText
  • 降低 resolution 以减少内存占用
遮罩
  • 矩形遮罩(裁剪)最快
  • 图形遮罩(模板)次之
  • 精灵遮罩(滤镜)开销较大
滤镜
  • 使用 container.filters = null 释放
  • 对于尺寸已知的对象,设置 filterArea
  • 谨慎使用——每个滤镜都会增加绘制调用
常规
  • 为大型场景启用剔除:cullable = true
  • 对静态内容使用 RenderGroups
  • 对非交互式容器设置 interactiveChildren = false

v8 迁移要点

主要变更
  1. 必须异步初始化
// OLD (v7)
const app = new Application({ width: 800 });

// NEW (v8)
const app = new Application();
await app.init({ width: 800 });
  1. Graphics API 已变更
// OLD (v7)
graphics.beginFill(0xff0000).drawRect(0, 0, 100, 100).endFill();

// NEW (v8)
graphics.rect(0, 0, 100, 100).fill(0xff0000);
  1. Ticker 回调
// OLD (v7)
ticker.add((dt) => sprite.rotation += dt);

// NEW (v8)
ticker.add((ticker) => sprite.rotation += ticker.deltaTime);
  1. 应用画布
// OLD (v7)
app.view

// NEW (v8)
app.canvas
  1. 叶节点不能有子项
  • Sprite、Graphics、Mesh 等不再能有子项
  • 改用 Container 作为父项
  1. getBounds 返回 Bounds
// OLD (v7)
const rect = container.getBounds();

// NEW (v8)
const rect = container.getBounds().rectangle;

Starwards 模式

CameraView 应用

Starwards 扩展了 Application,用于雷达/战术视图。

位置: modules/browser/src/radar/camera-view.ts

import { Application, ApplicationOptions, Container } from 'pixi.js';

export class CameraView extends Application {
  constructor(public camera: Camera) {
    super();
  }

  public async initialize(
    pixiOptions: Partial<ApplicationOptions>,
    container: WidgetContainer
  ) {
    await super.init(pixiOptions);

    // Limit FPS to prevent GPU heating
    this.ticker.maxFPS = 30;

    // Handle resize
    container.on('resize', () => {
      this.resizeView(container.width, container.height);
    });

    // Append canvas
    container.getElement().append(this.canvas);
  }

  // Coordinate transformations
  public worldToScreen = (w: XY) => this.camera.worldToScreen(this.renderer, w.x, w.y);
  public screenToWorld = (s: XY) => this.camera.screenToWorld(this.renderer, s.x, s.y);

  // Layer management
  public addLayer(child: Container) {
    this.stage.addChild(child);
  }
}

主要模式:

  • ticker.maxFPS = 30——防止过度占用 GPU
  • 坐标变换:worldToScreen()、screenToWorld()
  • 通过 addLayer() 组合图层

图层系统

Starwards 使用图层模式,每个图层都有一个 renderRoot Container。

GridLayer 示例

位置: modules/browser/src/radar/grid-layer.ts

import { Container, Graphics } from 'pixi.js';

export class GridLayer {
  private stage = new Container();
  private gridLines = new Graphics();

  constructor(private parent: CameraView) {
    this.parent.events.on('screenChanged', () => this.drawSectorGrid());
    this.stage.addChild(this.gridLines);
  }

  get renderRoot(): Container {
    return this.stage;
  }

  private drawSectorGrid() {
    // Clear and redraw
    this.gridLines.clear();

    // Draw lines using v8 API
    this.gridLines
      .moveTo(0, screen)
      .lineTo(this.parent.renderer.width, screen)
      .stroke({ width: 2, color: magnitude.color, alpha: 0.5 });
  }
}

模式:

  • 每个图层拥有一个 stage Container
  • 通过 renderRoot getter 暴露
  • 在 screenChanged 事件触发时重绘
  • 重绘前使用 graphics.clear()

Starwards 图形模式

v8 Fluent API 用法
// Drawing selection rectangle
const graphics = new Graphics();
graphics
  .rect(min.x, min.y, width, height)
  .fill({ color: selectionColor, alpha: 0.2 })
  .stroke({ width: 1, color: selectionColor, alpha: 1 });

// Drawing grid lines
this.gridLines
  .moveTo(0, screenY)
  .lineTo(rendererWidth, screenY)
  .stroke({ width: 2, color: lineColor, alpha: 0.5 });
清除并重绘模式
private redraw() {
  this.graphics.clear();
  // ... draw new content
}

Starwards 事件处理

位置: modules/browser/src/radar/interactive-layer.ts

设置
import { Container, FederatedPointerEvent, Rectangle } from 'pixi.js';

export class InteractiveLayer {
  private stage = new Container();

  constructor(private parent: CameraView) {
    // Set cursor
    this.stage.cursor = 'crosshair';

    // Enable interaction
    this.stage.interactive = true;

    // Set hit area to full canvas
    this.stage.hitArea = new Rectangle(
      0, 0,
      this.parent.renderer.width,
      this.parent.renderer.height
    );

    // Register events
    this.stage.on('pointerdown', this.onPointerDown);
    this.stage.on('pointermove', this.onPointerMove);
    this.stage.on('pointerup', this.onPointerUp);

    // Update hit area on resize
    this.parent.events.on('screenChanged', () => {
      this.stage.hitArea = new Rectangle(
        0, 0,
        this.parent.renderer.width,
        this.parent.renderer.height
      );
    });
  }

  private onPointerDown = (event: FederatedPointerEvent) => {
    const screenPos = XY.clone(event.global);
    const worldPos = this.parent.screenToWorld(screenPos);
    // ... handle interaction
  };
}

主要模式:

  • stage.interactive = true 启用事件
  • stage.hitArea = new Rectangle(...) 定义可点击区域
  • 调整大小时更新点击区域
  • 使用 event.global 获取屏幕坐标
  • 使用 screenToWorld() 转换为世界坐标

对象池

位置: modules/browser/src/radar/texts-pool.ts

Starwards 使用基于迭代器的对象池来减轻 GC 压力。

export class TextsPool {
  private texts: Text[] = [];

  constructor(private container: Container) {}

  *[Symbol.iterator]() {
    let index = 0;
    while (true) {
      if (index >= this.texts.length) {
        const text = new Text({ text: '', style: { ... } });
        this.texts.push(text);
        this.container.addChild(text);
      }
      const text = this.texts[index];
      text.visible = true;
      yield text;
      index++;
    }
  }

  return() {
    // Hide unused texts
    for (let i = this.usedCount; i < this.texts.length; i++) {
      this.texts[i].visible = false;
    }
  }
}

// Usage
const textsIterator = this.textsPool[Symbol.iterator]();
for (const item of items) {
  const text = textsIterator.next().value;
  text.text = item.label;
  text.x = item.x;
  text.y = item.y;
}
textsIterator.return(); // Hide unused

使用 Playwright 测试

数据属性

为画布元素添加 data-id,用于端到端测试:

this.canvas.setAttribute('data-id', 'Tactical Radar');
Playwright 选择器
// Select canvas by data-id
const canvas = page.locator('[data-id="Tactical Radar"]');

// Get attribute values
const zoom = await canvas.getAttribute('data-zoom');
RadarDriver 模式
class RadarDriver {
  constructor(private canvas: Locator) {}

  async getZoom() {
    return Number(await this.canvas.getAttribute('data-zoom'));
  }

  async setZoom(target: number) {
    await this.canvas.dispatchEvent('wheel', { deltaY: ... });
  }
}
测试注意事项
  • 不为 PixiJS 组件编写单元测试(因为测试的是视觉输出)
  • 使用 Playwright 进行端到端测试
  • 通过数据属性进行测试,而不是通过渲染后的像素
  • 在 Tweakpane 面板上使用 data-id:page.locator('[data-id="Panel Name"]')

快速参考

任务Starwards 模式
创建图层class MyLayer { stage = new Container(); get renderRoot() { return this.stage; } }
绘制图形graphics.rect(...).fill({...}).stroke({...})
重绘graphics.clear(); // then draw
交互stage.interactive = true; stage.hitArea = new Rectangle(...)
事件stage.on('pointerdown', handler)
坐标parent.worldToScreen(xy), parent.screenToWorld(xy)
FPS 限制ticker.maxFPS = 30
测试选择器page.locator('[data-id="..."]')

常见问题

  1. 使用 v7 Graphics API

    • 错误:beginFill()、drawRect()、endFill()
    • 正确:rect().fill().stroke()
  2. 忘记异步初始化

    • 错误:new Application({ width: 800 })
    • 正确:await app.init({ width: 800 })
  3. 向 Sprite 添加子节点

    • v8 叶节点不能有子节点
    • 使用 Container 作为父节点
  4. 未清除 Graphics

    • 重绘前调用 graphics.clear()
  5. 调整大小后未更新命中区域

    • 在画布调整大小时更新 stage.hitArea
  6. SVG 未能作为纹理加载

    • 对 SVG 使用 Texture.from()(GitHub issue #8694 中的解决方法)

评论

支持 Markdown