starwards-pixijs

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PixiJS v8 Development for Starwards

适用于Starwards的PixiJS v8开发

Overview

概述

Starwards uses PixiJS v8 (^8.14.0) for 2D rendering in the browser module. This skill covers both the PixiJS v8 API reference and Starwards-specific patterns.
Core principle: Layered container composition with ticker-driven updates synced to Colyseus state changes.
Starwards使用PixiJS v8 (^8.14.0) 在浏览器模块中实现2D渲染。本技能涵盖PixiJS v8 API参考以及Starwards专属开发模式。
核心原则: 基于分层容器组合,通过Ticker驱动更新并与Colyseus状态变化同步。

Table of Contents

目录

PixiJS v8 Reference

PixiJS v8参考

Application

Application类

The
Application
class provides an extensible entry point for PixiJS projects.
Application
类为PixiJS项目提供了可扩展的入口点。

Async Initialization (v8 Required)

异步初始化(v8强制要求)

typescript
import { Application } from 'pixi.js';

const app = new Application();

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

document.body.appendChild(app.canvas);
typescript
import { Application } from 'pixi.js';

const app = new Application();

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

document.body.appendChild(app.canvas);

Key Options

关键配置项

OptionTypeDefaultDescription
width
number
800
Initial width
height
number
600
Initial height
backgroundColor
ColorSource
'black'
Background color
antialias
boolean
-Enable anti-aliasing
resolution
number
1
Pixel resolution
resizeTo
Window | HTMLElement
-Auto-resize target
preference
'webgl' | 'webgpu'
'webgl'
Renderer type

配置项类型默认值描述
width
number
800
初始宽度
height
number
600
初始高度
backgroundColor
ColorSource
'black'
背景颜色
antialias
boolean
-启用抗锯齿
resolution
number
1
像素分辨率
resizeTo
Window | HTMLElement
-自动缩放目标
preference
'webgl' | 'webgpu'
'webgl'
渲染器类型

Containers & Scene Graph

容器与场景图

Creating Containers

创建容器

typescript
import { Container } from 'pixi.js';

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

app.stage.addChild(container);
typescript
import { Container } from 'pixi.js';

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

app.stage.addChild(container);

Parent-Child Relationships

父子容器关系

  • Children inherit transforms, alpha, visibility from parents
  • Render order: children render in insertion order (later = on top)
  • Use
    setChildIndex()
    or
    zIndex
    with
    sortableChildren
    for reordering
  • 子容器会继承父容器的变换、透明度和可见性
  • 渲染顺序:子容器按添加顺序渲染(后添加的在上层)
  • 使用
    setChildIndex()
    或结合
    sortableChildren
    zIndex
    进行重排序

Coordinate Systems

坐标系统

typescript
// Local to global
const globalPos = obj.toGlobal(new Point(0, 0));

// Global to local
const localPos = container.toLocal(new Point(100, 100));
typescript
// 局部坐标转全局坐标
const globalPos = obj.toGlobal(new Point(0, 0));

// 全局坐标转局部坐标
const localPos = container.toLocal(new Point(100, 100));

Culling

视口裁剪

typescript
container.cullable = true;        // Enable culling
container.cullableChildren = true; // Cull children recursively
container.cullArea = new Rectangle(0, 0, 400, 400); // Custom cull bounds

typescript
container.cullable = true;        // 启用视口裁剪
container.cullableChildren = true; // 递归裁剪子容器
container.cullArea = new Rectangle(0, 0, 400, 400); // 自定义裁剪范围

Sprites

精灵

Basic Usage

基础用法

typescript
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;
typescript
import { Sprite, Assets } from 'pixi.js';

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

sprite.anchor.set(0.5);  // 设置锚点为中心
sprite.x = 100;
sprite.y = 100;
sprite.tint = 0xff0000;  // 红色色调
sprite.alpha = 0.8;

Sprite Properties

精灵属性

PropertyDescription
texture
The texture to display
anchor
Origin point (0-1 range)
tint
Color tint
blendMode
Blend mode for compositing
width
,
height
Size (scales texture)

属性描述
texture
要显示的纹理
anchor
原点(范围0-1)
tint
颜色色调
blendMode
合成混合模式
width
,
height
尺寸(会缩放纹理)

Graphics (v8 API)

图形(v8 API)

CRITICAL: v8 uses a new fluent API. Build shapes first, then fill/stroke.
重要提示: v8采用了新的链式API。需先构建形状,再执行填充/描边操作。

v8 Fluent API

v8链式API

typescript
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 });
typescript
import { Graphics } from 'pixi.js';

// 先绘制形状,再填充/描边
const graphics = new Graphics()
  .rect(50, 50, 100, 100)
  .fill(0xff0000)
  .stroke({ width: 2, color: 'white' });

// 带填充和描边的圆形
const circle = new Graphics()
  .circle(100, 100, 50)
  .fill({ color: 0x00ff00, alpha: 0.5 })
  .stroke({ width: 3, color: 0x000000 });

Shape Methods

形状方法对比

v7 (OLD)v8 (NEW)
drawRect()
rect()
drawCircle()
circle()
drawEllipse()
ellipse()
drawRoundedRect()
roundRect()
drawPolygon()
poly()
drawStar()
star()
v7(旧版)v8(新版)
drawRect()
rect()
drawCircle()
circle()
drawEllipse()
ellipse()
drawRoundedRect()
roundRect()
drawPolygon()
poly()
drawStar()
star()

Lines

绘制线条

typescript
const lines = new Graphics()
  .moveTo(0, 0)
  .lineTo(100, 100)
  .lineTo(200, 0)
  .stroke({ width: 2, color: 0xff0000 });
typescript
const lines = new Graphics()
  .moveTo(0, 0)
  .lineTo(100, 100)
  .lineTo(200, 0)
  .stroke({ width: 2, color: 0xff0000 });

Holes (v8)

镂空效果(v8)

typescript
const rectWithHole = new Graphics()
  .rect(0, 0, 100, 100)
  .fill(0x00ff00)
  .circle(50, 50, 20)
  .cut();  // Creates hole
typescript
const rectWithHole = new Graphics()
  .rect(0, 0, 100, 100)
  .fill(0x00ff00)
  .circle(50, 50, 20)
  .cut();  // 创建镂空区域

GraphicsContext (Sharing)

GraphicsContext(共享图形数据)

typescript
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

typescript
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); // 共享同一图形数据

Text

文本

Basic Text

基础文本

typescript
import { Text, TextStyle } from 'pixi.js';

const text = new Text({
  text: 'Hello World',
  style: {
    fontFamily: 'Arial',
    fontSize: 24,
    fill: 0xffffff,
    align: 'center',
  },
});
typescript
import { Text, TextStyle } from 'pixi.js';

const text = new Text({
  text: 'Hello World',
  style: {
    fontFamily: 'Arial',
    fontSize: 24,
    fill: 0xffffff,
    align: 'center',
  },
});

TextStyle Properties

文本样式属性

PropertyDescription
fontFamily
Font name
fontSize
Size in pixels
fill
Fill color
stroke
Stroke settings
align
Text alignment
wordWrap
Enable word wrapping
wordWrapWidth
Wrap width
属性描述
fontFamily
字体名称
fontSize
字体大小(像素)
fill
填充颜色
stroke
描边设置
align
文本对齐方式
wordWrap
启用自动换行
wordWrapWidth
换行宽度

BitmapText (Performance)

BitmapText(高性能文本)

typescript
import { BitmapText } from 'pixi.js';

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

typescript
import { BitmapText } from 'pixi.js';

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

Textures & Assets

纹理与资源

Loading Assets

加载资源

typescript
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');
typescript
import { Assets, Sprite } from 'pixi.js';

// 加载单个资源
const texture = await Assets.load('path/to/image.png');
const sprite = new Sprite(texture);

// 加载多个资源
const textures = await Assets.load(['a.png', 'b.png']);

// 使用别名加载
await Assets.load({ alias: 'hero', src: 'images/hero.png' });
const heroTexture = Assets.get('hero');

Asset Bundles

资源包

typescript
Assets.addBundle('game', [
  { alias: 'player', src: 'player.png' },
  { alias: 'enemy', src: 'enemy.png' },
]);

const assets = await Assets.loadBundle('game');
typescript
Assets.addBundle('game', [
  { alias: 'player', src: 'player.png' },
  { alias: 'enemy', src: 'enemy.png' },
]);

const assets = await Assets.loadBundle('game');

Manifest

资源清单

typescript
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');
typescript
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');

SVGs

SVG资源

typescript
// 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);
typescript
// 作为纹理加载
const svgTexture = await Assets.load('icon.svg');
const sprite = new Sprite(svgTexture);

// 作为Graphics加载(可缩放)
const svgContext = await Assets.load({
  src: 'icon.svg',
  data: { parseAsGraphicsContext: true },
});
const graphics = new Graphics(svgContext);

Texture Cleanup

纹理清理

typescript
// Unload from cache and GPU
await Assets.unload('texture.png');

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

// Destroy texture
texture.destroy();

typescript
// 从缓存和GPU中卸载
await Assets.unload('texture.png');

// 仅从GPU卸载(保留在内存中)
texture.source.unload();

// 销毁纹理
texture.destroy();

Ticker

Ticker

Basic Usage

基础用法

typescript
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);
typescript
import { Ticker, UPDATE_PRIORITY } from 'pixi.js';

// 使用应用内置Ticker
app.ticker.add((ticker) => {
  sprite.rotation += 0.1 * ticker.deltaTime;
});

// 一次性回调
app.ticker.addOnce((ticker) => {
  console.log('仅调用一次');
});

// 带优先级的回调(数值越高越先执行)
app.ticker.add(updateFn, null, UPDATE_PRIORITY.HIGH);

Priority Constants

优先级常量

  • UPDATE_PRIORITY.HIGH = 50
  • UPDATE_PRIORITY.NORMAL = 0
  • UPDATE_PRIORITY.LOW = -50
  • UPDATE_PRIORITY.HIGH = 50
  • UPDATE_PRIORITY.NORMAL = 0
  • UPDATE_PRIORITY.LOW = -50

FPS Control

FPS控制

typescript
app.ticker.maxFPS = 60;  // Cap framerate
app.ticker.minFPS = 30;  // Clamp deltaTime
typescript
app.ticker.maxFPS = 60;  // 限制最大帧率
app.ticker.minFPS = 30;  // 限制最小deltaTime

Ticker Properties

Ticker属性

PropertyDescription
deltaTime
Scaled frame delta
elapsedMS
Raw milliseconds since last frame
FPS
Current frames per second

属性描述
deltaTime
缩放后的帧间隔
elapsedMS
上一帧以来的原始毫秒数
FPS
当前帧率

Events / Interaction

事件/交互

Event Modes

事件模式

typescript
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
typescript
sprite.eventMode = 'static';  // 可交互,不移动
sprite.eventMode = 'dynamic'; // 可交互,移动时接收空闲事件
sprite.eventMode = 'passive'; // 默认模式,子元素可交互
sprite.eventMode = 'none';    // 不可交互

Pointer Events

指针事件

typescript
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) => { /* ... */ });
typescript
sprite.eventMode = 'static';

sprite.on('pointerdown', (event) => {
  console.log('点击位置:', event.global.x, event.global.y);
});

sprite.on('pointermove', (event) => { /* ... */ });
sprite.on('pointerup', (event) => { /* ... */ });
sprite.on('pointerover', (event) => { /* ... */ });
sprite.on('pointerout', (event) => { /* ... */ });

Hit Area

点击区域

typescript
import { Rectangle, Circle } from 'pixi.js';

sprite.hitArea = new Rectangle(0, 0, 100, 100);
// or
sprite.hitArea = new Circle(50, 50, 50);
typescript
import { Rectangle, Circle } from 'pixi.js';

sprite.hitArea = new Rectangle(0, 0, 100, 100);
// 或者
sprite.hitArea = new Circle(50, 50, 50);

Custom Cursor

自定义光标

typescript
sprite.cursor = 'pointer';
sprite.cursor = 'grab';
sprite.cursor = 'url(cursor.png), auto';
typescript
sprite.cursor = 'pointer';
sprite.cursor = 'grab';
sprite.cursor = 'url(cursor.png), auto';

Disable Children Interaction

禁用子元素交互

typescript
container.interactiveChildren = false; // Skip children hit testing

typescript
container.interactiveChildren = false; // 跳过子元素点击检测

Performance Tips

性能优化技巧

Sprites

精灵

  • Use spritesheets to minimize texture switches
  • Sprites batch with up to 16 textures per batch
  • Draw order matters for batching efficiency
  • 使用精灵图集减少纹理切换
  • 精灵最多可批量处理16个纹理
  • 绘制顺序会影响批量处理效率

Graphics

图形

  • Graphics are fastest when not modified after creation
  • Small Graphics (<100 points) batch like sprites
  • Use sprites with textures for complex shapes
  • 图形创建后不修改时性能最佳
  • 小型图形(<100个顶点)可像精灵一样批量处理
  • 复杂形状建议使用带纹理的精灵

Text

文本

  • Avoid updating text every frame (expensive)
  • Use BitmapText for frequently changing text
  • Lower
    resolution
    for less memory
  • 避免每帧更新文本(性能开销大)
  • 频繁变化的文本使用BitmapText
  • 降低
    resolution
    减少内存占用

Masks

遮罩

  • Rectangle masks (scissor) are fastest
  • Graphics masks (stencil) are second fastest
  • Sprite masks (filters) are expensive
  • 矩形遮罩(裁剪)性能最佳
  • 图形遮罩(模板)次之
  • 精灵遮罩(滤镜)性能开销大

Filters

滤镜

  • Release with
    container.filters = null
  • Set
    filterArea
    for known dimensions
  • Use sparingly - each filter adds draw calls
  • 使用后通过
    container.filters = null
    释放
  • 为已知尺寸设置
    filterArea
  • 谨慎使用 - 每个滤镜都会增加绘制调用

General

通用优化

  • Enable culling for large scenes:
    cullable = true
  • Use
    RenderGroups
    for static content
  • Set
    interactiveChildren = false
    for non-interactive containers

  • 大型场景启用视口裁剪:
    cullable = true
  • 静态内容使用
    RenderGroups
  • 非交互容器设置
    interactiveChildren = false

v8 Migration Highlights

v8迁移要点

Key Changes

核心变化

  1. Async Initialization Required
typescript
// OLD (v7)
const app = new Application({ width: 800 });

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

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

// NEW (v8)
ticker.add((ticker) => sprite.rotation += ticker.deltaTime);
  1. Application Canvas
typescript
// OLD (v7)
app.view

// NEW (v8)
app.canvas
  1. Leaf Nodes Can't Have Children
  • Sprite
    ,
    Graphics
    ,
    Mesh
    etc. can no longer have children
  • Use
    Container
    as parent instead
  1. getBounds Returns Bounds
typescript
// OLD (v7)
const rect = container.getBounds();

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

  1. 强制要求异步初始化
typescript
// 旧版(v7)
const app = new Application({ width: 800 });

// 新版(v8)
const app = new Application();
await app.init({ width: 800 });
  1. 图形API变更
typescript
// 旧版(v7)
graphics.beginFill(0xff0000).drawRect(0, 0, 100, 100).endFill();

// 新版(v8)
graphics.rect(0, 0, 100, 100).fill(0xff0000);
  1. Ticker回调参数
typescript
// 旧版(v7)
ticker.add((dt) => sprite.rotation += dt);

// 新版(v8)
ticker.add((ticker) => sprite.rotation += ticker.deltaTime);
  1. 应用画布获取方式
typescript
// 旧版(v7)
app.view

// 新版(v8)
app.canvas
  1. 叶子节点不能有子元素
  • Sprite
    Graphics
    Mesh
    等不能再包含子元素
  • 需使用
    Container
    作为父容器
  1. getBounds返回值变更
typescript
// 旧版(v7)
const rect = container.getBounds();

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

Starwards Patterns

Starwards开发模式

CameraView Application

CameraView应用

Starwards extends
Application
for radar/tactical views.
Location:
modules/browser/src/radar/camera-view.ts
typescript
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);
  }
}
Key patterns:
  • ticker.maxFPS = 30
    - Prevents excessive GPU usage
  • Coordinate transforms:
    worldToScreen()
    ,
    screenToWorld()
  • Layer composition via
    addLayer()

Starwards扩展了
Application
类用于雷达/战术视图。
代码位置:
modules/browser/src/radar/camera-view.ts
typescript
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);

    // 限制FPS防止GPU过热
    this.ticker.maxFPS = 30;

    // 处理窗口缩放
    container.on('resize', () => {
      this.resizeView(container.width, container.height);
    });

    // 将画布添加到DOM
    container.getElement().append(this.canvas);
  }

  // 坐标转换方法
  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);

  // 图层管理
  public addLayer(child: Container) {
    this.stage.addChild(child);
  }
}
核心模式:
  • ticker.maxFPS = 30
    - 避免GPU过度占用
  • 坐标转换:
    worldToScreen()
    screenToWorld()
  • 通过
    addLayer()
    实现图层组合

Layer System

分层系统

Starwards uses a layer pattern where each layer has a
renderRoot
Container.
Starwards采用分层模式,每个图层拥有一个
renderRoot
容器。

GridLayer Example

GridLayer示例

Location:
modules/browser/src/radar/grid-layer.ts
typescript
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 });
  }
}
Pattern:
  • Each layer owns a
    stage
    Container
  • Exposes via
    renderRoot
    getter
  • Redraws on
    screenChanged
    event
  • Uses
    graphics.clear()
    before redrawing

代码位置:
modules/browser/src/radar/grid-layer.ts
typescript
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() {
    // 清空并重绘
    this.gridLines.clear();

    // 使用v8 API绘制线条
    this.gridLines
      .moveTo(0, screen)
      .lineTo(this.parent.renderer.width, screen)
      .stroke({ width: 2, color: magnitude.color, alpha: 0.5 });
  }
}
设计模式:
  • 每个图层拥有独立的
    stage
    容器
  • 通过
    renderRoot
    getter暴露容器
  • 监听
    screenChanged
    事件触发重绘
  • 重绘前调用
    graphics.clear()

Starwards Graphics Patterns

Starwards图形开发模式

v8 Fluent API Usage

v8链式API使用示例

typescript
// 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 });
typescript
// 绘制选择矩形
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 });

// 绘制网格线
this.gridLines
  .moveTo(0, screenY)
  .lineTo(rendererWidth, screenY)
  .stroke({ width: 2, color: lineColor, alpha: 0.5 });

Clear and Redraw Pattern

清空重绘模式

typescript
private redraw() {
  this.graphics.clear();
  // ... draw new content
}

typescript
private redraw() {
  this.graphics.clear();
  // ... 绘制新内容
}

Starwards Event Handling

Starwards事件处理

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

Setup

初始化设置

typescript
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
  };
}
Key patterns:
  • stage.interactive = true
    enables events
  • stage.hitArea = new Rectangle(...)
    defines clickable area
  • Update hit area on resize
  • Use
    event.global
    for screen coordinates
  • Convert to world with
    screenToWorld()

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

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

  constructor(private parent: CameraView) {
    // 设置光标样式
    this.stage.cursor = 'crosshair';

    // 启用交互
    this.stage.interactive = true;

    // 设置点击区域为整个画布
    this.stage.hitArea = new Rectangle(
      0, 0,
      this.parent.renderer.width,
      this.parent.renderer.height
    );

    // 注册事件回调
    this.stage.on('pointerdown', this.onPointerDown);
    this.stage.on('pointermove', this.onPointerMove);
    this.stage.on('pointerup', this.onPointerUp);

    // 窗口缩放时更新点击区域
    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);
    // ... 处理交互逻辑
  };
}
核心模式:
  • stage.interactive = true
    启用事件交互
  • stage.hitArea = new Rectangle(...)
    定义可点击区域
  • 窗口缩放时更新点击区域
  • 使用
    event.global
    获取屏幕坐标
  • 通过
    screenToWorld()
    转换为世界坐标

Object Pooling

对象池

Location:
modules/browser/src/radar/texts-pool.ts
Starwards uses iterator-based pooling to reduce GC pressure.
typescript
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

代码位置:
modules/browser/src/radar/texts-pool.ts
Starwards使用迭代器实现对象池,减少GC压力。
typescript
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() {
    // 隐藏未使用的文本
    for (let i = this.usedCount; i < this.texts.length; i++) {
      this.texts[i].visible = false;
    }
  }
}

// 使用示例
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(); // 隐藏未使用的文本

Testing with Playwright

使用Playwright测试

Data Attributes

数据属性标记

Add
data-id
to canvas elements for E2E testing:
typescript
this.canvas.setAttribute('data-id', 'Tactical Radar');
为画布元素添加
data-id
属性用于端到端测试:
typescript
this.canvas.setAttribute('data-id', 'Tactical Radar');

Playwright Selectors

Playwright选择器

typescript
// Select canvas by data-id
const canvas = page.locator('[data-id="Tactical Radar"]');

// Get attribute values
const zoom = await canvas.getAttribute('data-zoom');
typescript
// 通过data-id选择画布
const canvas = page.locator('[data-id="Tactical Radar"]');

// 获取属性值
const zoom = await canvas.getAttribute('data-zoom');

RadarDriver Pattern

RadarDriver模式

typescript
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: ... });
  }
}
typescript
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: ... });
  }
}

Testing Considerations

测试注意事项

  • No unit tests for PixiJS components (visual output)
  • Use E2E tests with Playwright
  • Test via data attributes, not rendered pixels
  • Use
    data-id
    on Tweakpane panels:
    page.locator('[data-id="Panel Name"]')

  • PixiJS组件不编写单元测试(依赖视觉输出)
  • 使用Playwright进行端到端测试
  • 通过数据属性而非渲染像素进行测试
  • 为Tweakpane面板添加
    data-id
    page.locator('[data-id="Panel Name"]')

Quick Reference

快速参考

TaskStarwards Pattern
Create layer
class MyLayer { stage = new Container(); get renderRoot() { return this.stage; } }
Draw graphics
graphics.rect(...).fill({...}).stroke({...})
Redraw
graphics.clear(); // then draw
Interactive
stage.interactive = true; stage.hitArea = new Rectangle(...)
Events
stage.on('pointerdown', handler)
Coords
parent.worldToScreen(xy)
,
parent.screenToWorld(xy)
FPS limit
ticker.maxFPS = 30
Test selector
page.locator('[data-id="..."]')

任务Starwards实现模式
创建图层
class MyLayer { stage = new Container(); get renderRoot() { return this.stage; } }
绘制图形
graphics.rect(...).fill({...}).stroke({...})
重绘图形
graphics.clear(); // 然后绘制新内容
启用交互
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="..."]')

Common Pitfalls

常见误区

  1. Using v7 Graphics API
    • Wrong:
      beginFill()
      ,
      drawRect()
      ,
      endFill()
    • Right:
      rect().fill().stroke()
  2. Forgetting async init
    • Wrong:
      new Application({ width: 800 })
    • Right:
      await app.init({ width: 800 })
  3. Adding children to Sprites
    • v8 leaf nodes can't have children
    • Use Container as parent
  4. Not clearing Graphics
    • Call
      graphics.clear()
      before redrawing
  5. Hit area not updated on resize
    • Update
      stage.hitArea
      when canvas resizes
  6. SVGs not loading as textures
    • Use
      Texture.from()
      for SVGs (GitHub issue #8694 workaround)
  1. 使用v7图形API
    • 错误写法:
      beginFill()
      ,
      drawRect()
      ,
      endFill()
    • 正确写法:
      rect().fill().stroke()
  2. 忘记异步初始化
    • 错误写法:
      new Application({ width: 800 })
    • 正确写法:
      await app.init({ width: 800 })
  3. 为精灵添加子元素
    • v8中叶子节点不能包含子元素
    • 需使用Container作为父容器
  4. 未清空图形就重绘
    • 重绘前需调用
      graphics.clear()
  5. 窗口缩放时未更新点击区域
    • 画布尺寸变化时需更新
      stage.hitArea
  6. SVG无法作为纹理加载
    • 使用
      Texture.from()
      加载SVG(GitHub Issue #8694的临时解决方案)