starwards-pixijs
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePixiJS 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 class provides an extensible entry point for PixiJS projects.
ApplicationApplicationAsync 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
关键配置项
| Option | Type | Default | Description |
|---|---|---|---|
| | | Initial width |
| | | Initial height |
| | | Background color |
| | - | Enable anti-aliasing |
| | | Pixel resolution |
| | - | Auto-resize target |
| | | Renderer type |
| 配置项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| | | 初始宽度 |
| | | 初始高度 |
| | | 背景颜色 |
| | - | 启用抗锯齿 |
| | | 像素分辨率 |
| | - | 自动缩放目标 |
| | | 渲染器类型 |
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 or
setChildIndex()withzIndexfor reorderingsortableChildren
- 子容器会继承父容器的变换、透明度和可见性
- 渲染顺序:子容器按添加顺序渲染(后添加的在上层)
- 使用或结合
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 boundstypescript
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
精灵属性
| Property | Description |
|---|---|
| The texture to display |
| Origin point (0-1 range) |
| Color tint |
| Blend mode for compositing |
| Size (scales texture) |
| 属性 | 描述 |
|---|---|
| 要显示的纹理 |
| 原点(范围0-1) |
| 颜色色调 |
| 合成混合模式 |
| 尺寸(会缩放纹理) |
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) |
|---|---|
| |
| |
| |
| |
| |
| |
| v7(旧版) | v8(新版) |
|---|---|
| |
| |
| |
| |
| |
| |
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 holetypescript
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 datatypescript
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
文本样式属性
| Property | Description |
|---|---|
| Font name |
| Size in pixels |
| Fill color |
| Stroke settings |
| Text alignment |
| Enable word wrapping |
| Wrap width |
| 属性 | 描述 |
|---|---|
| 字体名称 |
| 字体大小(像素) |
| 填充颜色 |
| 描边设置 |
| 文本对齐方式 |
| 启用自动换行 |
| 换行宽度 |
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 = 50UPDATE_PRIORITY.NORMAL = 0UPDATE_PRIORITY.LOW = -50
UPDATE_PRIORITY.HIGH = 50UPDATE_PRIORITY.NORMAL = 0UPDATE_PRIORITY.LOW = -50
FPS Control
FPS控制
typescript
app.ticker.maxFPS = 60; // Cap framerate
app.ticker.minFPS = 30; // Clamp deltaTimetypescript
app.ticker.maxFPS = 60; // 限制最大帧率
app.ticker.minFPS = 30; // 限制最小deltaTimeTicker Properties
Ticker属性
| Property | Description |
|---|---|
| Scaled frame delta |
| Raw milliseconds since last frame |
| Current frames per second |
| 属性 | 描述 |
|---|---|
| 缩放后的帧间隔 |
| 上一帧以来的原始毫秒数 |
| 当前帧率 |
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 interactiontypescript
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 testingtypescript
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 for less memory
resolution
- 避免每帧更新文本(性能开销大)
- 频繁变化的文本使用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 for known dimensions
filterArea - Use sparingly - each filter adds draw calls
- 使用后通过释放
container.filters = null - 为已知尺寸设置
filterArea - 谨慎使用 - 每个滤镜都会增加绘制调用
General
通用优化
- Enable culling for large scenes:
cullable = true - Use for static content
RenderGroups - Set for non-interactive containers
interactiveChildren = false
- 大型场景启用视口裁剪:
cullable = true - 静态内容使用
RenderGroups - 非交互容器设置
interactiveChildren = false
v8 Migration Highlights
v8迁移要点
Key Changes
核心变化
- Async Initialization Required
typescript
// OLD (v7)
const app = new Application({ width: 800 });
// NEW (v8)
const app = new Application();
await app.init({ width: 800 });- 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);- Ticker Callback
typescript
// OLD (v7)
ticker.add((dt) => sprite.rotation += dt);
// NEW (v8)
ticker.add((ticker) => sprite.rotation += ticker.deltaTime);- Application Canvas
typescript
// OLD (v7)
app.view
// NEW (v8)
app.canvas- Leaf Nodes Can't Have Children
- ,
Sprite,Graphicsetc. can no longer have childrenMesh - Use as parent instead
Container
- getBounds Returns Bounds
typescript
// OLD (v7)
const rect = container.getBounds();
// NEW (v8)
const rect = container.getBounds().rectangle;- 强制要求异步初始化
typescript
// 旧版(v7)
const app = new Application({ width: 800 });
// 新版(v8)
const app = new Application();
await app.init({ width: 800 });- 图形API变更
typescript
// 旧版(v7)
graphics.beginFill(0xff0000).drawRect(0, 0, 100, 100).endFill();
// 新版(v8)
graphics.rect(0, 0, 100, 100).fill(0xff0000);- Ticker回调参数
typescript
// 旧版(v7)
ticker.add((dt) => sprite.rotation += dt);
// 新版(v8)
ticker.add((ticker) => sprite.rotation += ticker.deltaTime);- 应用画布获取方式
typescript
// 旧版(v7)
app.view
// 新版(v8)
app.canvas- 叶子节点不能有子元素
- 、
Sprite、Graphics等不能再包含子元素Mesh - 需使用作为父容器
Container
- getBounds返回值变更
typescript
// 旧版(v7)
const rect = container.getBounds();
// 新版(v8)
const rect = container.getBounds().rectangle;Starwards Patterns
Starwards开发模式
CameraView Application
CameraView应用
Starwards extends for radar/tactical views.
ApplicationLocation:
modules/browser/src/radar/camera-view.tstypescript
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:
- - Prevents excessive GPU usage
ticker.maxFPS = 30 - Coordinate transforms: ,
worldToScreen()screenToWorld() - Layer composition via
addLayer()
Starwards扩展了类用于雷达/战术视图。
Application代码位置:
modules/browser/src/radar/camera-view.tstypescript
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);
}
}核心模式:
- - 避免GPU过度占用
ticker.maxFPS = 30 - 坐标转换:、
worldToScreen()screenToWorld() - 通过实现图层组合
addLayer()
Layer System
分层系统
Starwards uses a layer pattern where each layer has a Container.
renderRootStarwards采用分层模式,每个图层拥有一个容器。
renderRootGridLayer Example
GridLayer示例
Location:
modules/browser/src/radar/grid-layer.tstypescript
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 Container
stage - Exposes via getter
renderRoot - Redraws on event
screenChanged - Uses before redrawing
graphics.clear()
代码位置:
modules/browser/src/radar/grid-layer.tstypescript
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 - 通过getter暴露容器
renderRoot - 监听事件触发重绘
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.tsSetup
初始化设置
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:
- enables events
stage.interactive = true - defines clickable area
stage.hitArea = new Rectangle(...) - Update hit area on resize
- Use for screen coordinates
event.global - 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.tsStarwards 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.tsStarwards使用迭代器实现对象池,减少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 to canvas elements for E2E testing:
data-idtypescript
this.canvas.setAttribute('data-id', 'Tactical Radar');为画布元素添加属性用于端到端测试:
data-idtypescript
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 on Tweakpane panels:
data-idpage.locator('[data-id="Panel Name"]')
- PixiJS组件不编写单元测试(依赖视觉输出)
- 使用Playwright进行端到端测试
- 通过数据属性而非渲染像素进行测试
- 为Tweakpane面板添加:
data-idpage.locator('[data-id="Panel Name"]')
Quick Reference
快速参考
| Task | Starwards Pattern |
|---|---|
| Create layer | |
| Draw graphics | |
| Redraw | |
| Interactive | |
| Events | |
| Coords | |
| FPS limit | |
| Test selector | |
| 任务 | Starwards实现模式 |
|---|---|
| 创建图层 | |
| 绘制图形 | |
| 重绘图形 | |
| 启用交互 | |
| 绑定事件 | |
| 坐标转换 | |
| 限制FPS | |
| 测试选择器 | |
Common Pitfalls
常见误区
-
Using v7 Graphics API
- Wrong: ,
beginFill(),drawRect()endFill() - Right:
rect().fill().stroke()
- Wrong:
-
Forgetting async init
- Wrong:
new Application({ width: 800 }) - Right:
await app.init({ width: 800 })
- Wrong:
-
Adding children to Sprites
- v8 leaf nodes can't have children
- Use Container as parent
-
Not clearing Graphics
- Call before redrawing
graphics.clear()
- Call
-
Hit area not updated on resize
- Update when canvas resizes
stage.hitArea
- Update
-
SVGs not loading as textures
- Use for SVGs (GitHub issue #8694 workaround)
Texture.from()
- Use
-
使用v7图形API
- 错误写法:,
beginFill(),drawRect()endFill() - 正确写法:
rect().fill().stroke()
- 错误写法:
-
忘记异步初始化
- 错误写法:
new Application({ width: 800 }) - 正确写法:
await app.init({ width: 800 })
- 错误写法:
-
为精灵添加子元素
- v8中叶子节点不能包含子元素
- 需使用Container作为父容器
-
未清空图形就重绘
- 重绘前需调用
graphics.clear()
- 重绘前需调用
-
窗口缩放时未更新点击区域
- 画布尺寸变化时需更新
stage.hitArea
- 画布尺寸变化时需更新
-
SVG无法作为纹理加载
- 使用加载SVG(GitHub Issue #8694的临时解决方案)
Texture.from()
- 使用