Skip to main content

场景图

¥Scene Graph

每一帧,PixiJS 都会更新并渲染场景图。让我们讨论一下场景图中的内容,以及它如何影响你开发项目的方式。如果你以前构建过游戏,这听起来应该非常熟悉,但如果你来自 HTML 和 DOM,那么在我们讨论可以渲染的特定对象类型之前,值得先了解一下。

¥Every frame, PixiJS is updating and then rendering the scene graph. Let's talk about what's in the scene graph, and how it impacts how you develop your project. If you've built games before, this should all sound very familiar, but if you're coming from HTML and the DOM, it's worth understanding before we get into specific types of objects you can render.

场景图是一棵树

¥The Scene Graph Is a Tree

场景图的根节点是由应用维护的容器,并用 app.stage 引用。当你将精灵或其他可渲染对象作为子对象添加到舞台时,它会添加到场景图中,并且将被渲染和交互。PixiJS Containers 也可以有子级,因此当你构建更复杂的场景时,你最终会得到一棵植根于应用阶段的父子关系树。

¥The scene graph's root node is a container maintained by the application, and referenced with app.stage. When you add a sprite or other renderable object as a child to the stage, it's added to the scene graph and will be rendered and interactable. PixiJS Containers can also have children, and so as you build more complex scenes, you will end up with a tree of parent-child relationships, rooted at the app's stage.

(探索项目的一个有用工具是 Pixi.js 开发工具插件 for Chrome,它允许你在场景图运行时实时查看和操作场景图!)

¥(A helpful tool for exploring your project is the Pixi.js devtools plugin for Chrome, which allows you to view and manipulate the scene graph in real time as it's running!)

父级和子级

¥Parents and Children

当父级移动时,其子级也会移动。当父级旋转时,其子级也会旋转。隐藏父级,子级也会被隐藏。如果你有一个由多个精灵组成的游戏对象,你可以将它们收集在一个容器下,将它们视为世界中的单个对象,作为一个整体移动和旋转。

¥When a parent moves, its children move as well. When a parent is rotated, its children are rotated too. Hide a parent, and the children will also be hidden. If you have a game object that's made up of multiple sprites, you can collect them under a container to treat them as a single object in the world, moving and rotating as one.

每一帧,PixiJS 都会从根向下遍历所有子对象到叶子,以计算每个对象的最终位置、旋转、可见性、透明度等。如果父对象的 alpha 设置为 0.5(使其 50% 透明) ,其所有子项也将从 50% 透明开始。如果将子项设置为 0.5 alpha,则它不会是 50% 透明,而是 0.5 x 0.5 = 0.25 alpha,或 75% 透明。同样,对象的位置是相对于其父对象的,因此如果将父对象的 x 位置设置为 50 像素,将子对象的 x 位置设置为 100 像素,则将以 150 像素的屏幕偏移量绘制对象, 或 50 + 100。

¥Each frame, PixiJS runs through the scene graph from the root down through all the children to the leaves to calculate each object's final position, rotation, visibility, transparency, etc. If a parent's alpha is set to 0.5 (making it 50% transparent), all its children will start at 50% transparent as well. If a child is then set to 0.5 alpha, it won't be 50% transparent, it will be 0.5 x 0.5 = 0.25 alpha, or 75% transparent. Similarly, an object's position is relative to its parent, so if a parent is set to an x position of 50 pixels, and the child is set to an x position of 100 pixels, it will be drawn at a screen offset of 150 pixels, or 50 + 100.

这是一个例子。我们将创建三个精灵,每个精灵都是最后一个精灵的子级,并为它们的位置、旋转、缩放和 Alpha 设置动画。即使每个精灵的属性设置为相同的值,父子链也会放大每个更改:

¥Here's an example. We'll create three sprites, each a child of the last, and animate their position, rotation, scale and alpha. Even though each sprite's properties are set to the same values, the parent-child chain amplifies each change:

// Create the application helper and add its render target to the page
const app = new Application();
await app.init({ width: 640, height: 360 })
document.body.appendChild(app.canvas);

// Add a container to center our sprite stack on the page
const container = new Container({
x:app.screen.width / 2,
y:app.screen.height / 2
});

app.stage.addChild(container);

// load the texture
await Assets.load('assets/images/sample.png');

// Create the 3 sprites, each a child of the last
const sprites = [];
let parent = container;
for (let i = 0; i < 3; i++) {
let wrapper = new Container();
let sprite = Sprite.from('assets/images/sample.png');
sprite.anchor.set(0.5);
wrapper.addChild(sprite);
parent.addChild(wrapper);
sprites.push(wrapper);
parent = wrapper;
}

// Set all sprite's properties to the same value, animated over time
let elapsed = 0.0;
app.ticker.add((delta) => {
elapsed += delta.deltaTime / 60;
const amount = Math.sin(elapsed);
const scale = 1.0 + 0.25 * amount;
const alpha = 0.75 + 0.25 * amount;
const angle = 40 * amount;
const x = 75 * amount;
for (let i = 0; i < sprites.length; i++) {
const sprite = sprites[i];
sprite.scale.set(scale);
sprite.alpha = alpha;
sprite.angle = angle;
sprite.x = x;
}
});

场景图中任何给定节点的累积平移、旋转、缩放和倾斜都存储在对象的 worldTransform 属性中。同样,累积 alpha 值存储在 worldAlpha 属性中。

¥The cumulative translation, rotation, scale and skew of any given node in the scene graph is stored in the object's worldTransform property. Similarly, the cumulative alpha value is stored in the worldAlpha property.

渲染顺序

¥Render Order

所以我们有一棵树要画。谁先被抽中?

¥So we have a tree of things to draw. Who gets drawn first?

PixiJS 从根部向下渲染树。在每个级别,渲染当前对象,然后按插入顺序渲染每个子对象。因此,第二个子项渲染在第一个子项之上,第三个子项渲染在第二个子项之上。

¥PixiJS renders the tree from the root down. At each level, the current object is rendered, then each child is rendered in order of insertion. So the second child is rendered on top of the first child, and the third over the second.

查看此示例,其中 A 下有两个父对象 A 和 D,以及两个子对象 B 和 C:

¥Check out this example, with two parent objects A & D, and two children B & C under A:

// Create the application helper and add its render target to the page
const app = new Application();
await app.init({ width: 640, height: 360 })
document.body.appendChild(app.canvas);

// Label showing scene graph hierarchy
const label = new Text({
text:'Scene Graph:\n\napp.stage\n ┗ A\n ┗ B\n ┗ C\n ┗ D',
style:{fill: '#ffffff'},
position: {x: 300, y: 100}
});

app.stage.addChild(label);

// Helper function to create a block of color with a letter
const letters = [];
function addLetter(letter, parent, color, pos) {
const bg = new Sprite(Texture.WHITE);
bg.width = 100;
bg.height = 100;
bg.tint = color;

const text = new Text({
text:letter,
style:{fill: "#ffffff"}
});

text.anchor.set(0.5);
text.position = {x: 50, y: 50};

const container = new Container();
container.position = pos;
container.visible = false;
container.addChild(bg, text);
parent.addChild(container);

letters.push(container);
return container;
}

// Define 4 letters
let a = addLetter('A', app.stage, 0xff0000, {x: 100, y: 100});
let b = addLetter('B', a, 0x00ff00, {x: 20, y: 20});
let c = addLetter('C', a, 0x0000ff, {x: 20, y: 40});
let d = addLetter('D', app.stage, 0xff8800, {x: 140, y: 100});

// Display them over time, in order
let elapsed = 0.0;
app.ticker.add((ticker) => {
elapsed += ticker.deltaTime / 60.0;
if (elapsed >= letters.length) { elapsed = 0.0; }
for (let i = 0; i < letters.length; i ++) {
letters[i].visible = elapsed >= i;
}
});

如果你想重新排序子对象,可以使用 setChildIndex()。要在父级列表中的给定点添加子级,请使用 addChildAt()。最后,你可以使用 sortableChildren 选项并结合为每个子对象设置 zIndex 属性来启用对象子对象的自动排序。

¥If you'd like to re-order a child object, you can use setChildIndex(). To add a child at a given point in a parent's list, use addChildAt(). Finally, you can enable automatic sorting of an object's children using the sortableChildren option combined with setting the zIndex property on each child.

RenderGroups

当你深入研究 PixiJS 时,你将遇到一个称为渲染组的强大功能。将渲染组视为场景图中的专用容器,其作用类似于迷你场景图本身。以下是你在项目中有效使用渲染组需要了解的内容。欲了解更多信息,请查看 渲染组概述

¥As you delve deeper into PixiJS, you'll encounter a powerful feature known as Render Groups. Think of Render Groups as specialized containers within your scene graph that act like mini scene graphs themselves. Here's what you need to know to effectively use Render Groups in your projects. For more info check out the RenderGroups overview

剔除

¥Culling

如果你正在构建一个项目,其中大部分场景对象都在屏幕外(例如,横向滚动游戏),你将需要剔除这些对象。剔除是评估对象(或其子对象!)是否在屏幕上的过程,如果不在屏幕上,则关闭其渲染。如果你不剔除屏幕外的对象,渲染器仍会绘制它们,即使它们的任何像素最终都不会出现在屏幕上。

¥If you're building a project where a large proportion of your scene objects are off-screen (say, a side-scrolling game), you will want to cull those objects. Culling is the process of evaluating if an object (or its children!) is on the screen, and if not, turning off rendering for it. If you don't cull off-screen objects, the renderer will still draw them, even though none of their pixels end up on the screen.

PixiJS 不提供对视口剔除的内置支持,但你可以找到可能满足你需求的第 3 方插件。或者,如果你想构建自己的剔除系统,只需在每个刻度期间运行对象并将不需要绘制的任何对象上的 renderable 设置为 false 即可。

¥PixiJS doesn't provide built-in support for viewport culling, but you can find 3rd party plugins that might fit your needs. Alternately, if you'd like to build your own culling system, simply run your objects during each tick and set renderable to false on any object that doesn't need to be drawn.

本地坐标与全局坐标

¥Local vs Global Coordinates

如果你将精灵添加到舞台,默认情况下它将显示在屏幕的左上角。这就是 PixiJS 使用的全局坐标空间的起源。如果你的所有对象都是舞台的子对象,那么这是你需要担心的唯一坐标。但一旦引入容器和子元素,事情就会变得更加复杂。[50, 100] 处的子对象距离其父对象向右 50 像素,向下 100 像素。

¥If you add a sprite to the stage, by default it will show up in the top left corner of the screen. That's the origin of the global coordinate space used by PixiJS. If all your objects were children of the stage, that's the only coordinates you'd need to worry about. But once you introduce containers and children, things get more complicated. A child object at [50, 100] is 50 pixels right and 100 pixels down from its parent.

我们将这两个坐标系称为 "global" 和 "local" 坐标。当你在对象上使用 position.set(x, y) 时,你始终使用相对于该对象的父对象的本地坐标。

¥We call these two coordinate systems "global" and "local" coordinates. When you use position.set(x, y) on an object, you're always working in local coordinates, relative to the object's parent.

问题是,很多时候你想知道一个对象的全局位置。例如,如果你想要剔除屏幕外对象以节省渲染时间,你需要知道给定的子对象是否位于视图矩形之外。

¥The problem is, there are many times when you want to know the global position of an object. For example, if you want to cull offscreen objects to save render time, you need to know if a given child is outside the view rectangle.

要从局部坐标转换为全局坐标,请使用 toGlobal() 函数。这是一个示例用法:

¥To convert from local to global coordinates, you use the toGlobal() function. Here's a sample usage:

// Get the global position of an object, relative to the top-left of the screen
let globalPos = obj.toGlobal(new Point(0,0));

此代码片段将 globalPos 设置为子对象的全局坐标,相对于全局坐标系中的 [0, 0]

¥This snippet will set globalPos to be the global coordinates for the child object, relative to [0, 0] in the global coordinate system.

全局坐标与屏幕坐标

¥Global vs Screen Coordinates

当你的项目使用主机操作系统或浏览器时,就会有第三个坐标系发挥作用 - "screen" 坐标(又名 "viewport" 坐标)。屏幕坐标表示相对于 PixiJS 渲染到的画布元素左上角的位置。DOM 和原生鼠标单击事件之类的东西在屏幕空间中工作。

¥When your project is working with the host operating system or browser, there is a third coordinate system that comes into play - "screen" coordinates (aka "viewport" coordinates). Screen coordinates represent position relative to the top-left of the canvas element that PixiJS is rendering into. Things like the DOM and native mouse click events work in screen space.

现在,在许多情况下,屏幕空间相当于世界空间。如果画布的大小与创建 Application 时指定的渲染视图的大小相同,就会出现这种情况。默认情况下是这样的 - 例如,你将创建一个 800x600 的应用窗口并将其添加到你的 HTML 页面,并且它将保持该大小。世界坐标中的 100 个像素将等于屏幕空间中的 100 个像素。但!通常会拉伸渲染的视图以使其填满屏幕,或者以较低的分辨率和较高的比例进行渲染以提高速度。在这种情况下,canvas 元素的屏幕大小将会改变(例如通过 CSS),但底层渲染视图不会改变,从而导致世界坐标和屏幕坐标之间不匹配。

¥Now, in many cases, screen space is equivalent to world space. This is the case if the size of the canvas is the same as the size of the render view specified when you create you Application. By default, this will be the case - you'll create for example an 800x600 application window and add it to your HTML page, and it will stay that size. 100 pixels in world coordinates will equal 100 pixels in screen space. BUT! It is common to stretch the rendered view to have it fill the screen, or to render at a lower resolution and up-scale for speed. In that case, the screen size of the canvas element will change (e.g. via CSS), but the underlying render view will not, resulting in a mis-match between world coordinates and screen coordinates.