SpriteKit 在哪里为数千个精灵加载纹理图集
Posted
技术标签:
【中文标题】SpriteKit 在哪里为数千个精灵加载纹理图集【英文标题】:SpriteKit where to load texture atlases for thousands of sprites 【发布时间】:2013-10-21 15:23:27 【问题描述】:在我的游戏中,我有数以千计的“平铺”节点组成游戏地图(想想 simcity),我想知道为每个节点设置纹理和动画的帧速率/内存效率最高的路线是什么?有一些独特的瓷砖“类型”,每个都有自己的纹理图集/动画,因此确保尽可能重复使用纹理是关键。
我所有的 tile
节点都是单个 map
节点的子节点,地图节点是否应该处理识别图块类型并加载必要的图集和动画(例如,通过从 plist 加载纹理和图集名称?)
或者,每个图块类型都是某个子类。每个 SKSpriteNode 瓦片处理自己的精灵图集加载会更好吗? [tileInstance texturise];
(sprite kit 是如何处理这个问题的?这种方法是否会导致为某个图块类型的每个实例将相同的纹理图集加载到内存中?)
我一直在寻找有关图集和纹理重用的更深入解释的文档,但我不知道此类场景的典型程序是什么。任何帮助将不胜感激,谢谢。
【问题讨论】:
【参考方案1】:内存首先:不会有任何明显的差异。您必须加载 tile 的纹理,纹理将至少占 Map+Tiles 内存的 99%,仅此而已。
纹理重用:纹理被自动重用/缓存。使用相同纹理的两个精灵将引用相同的纹理,而不是每个精灵都有自己的纹理副本。
帧率/批处理:这都是关于正确批处理的。 Sprite Kit 通过按添加到 children 数组的顺序渲染节点的子节点来处理它们的批处理。只要下一个子节点使用与前一个相同的纹理,它们都会被批处理到一个绘制调用中。可能你能做的最糟糕的事情就是添加一个精灵、一个标签、一个精灵、一个标签等等。您需要尽可能多地使用相同的纹理以连续的顺序添加精灵。
Atlas 使用情况:这是您最能获胜的地方。通常,开发人员会尝试对他们的地图集进行分类,这是错误的做法。与其为每个图块(及其动画)创建一个图集,不如创建尽可能少的纹理图集,每个图集包含尽可能多的图块。在所有 ios 7 设备上,纹理图集可以是 2048x2048,除了 iPhone 4 和 iPad 1 之外,所有其他设备都可以使用高达 4096x4096 像素的纹理。
这条规则也有例外,比如说,如果你有大量的纹理,你不可能一次将它们全部加载到所有设备的内存中。在这种情况下,请使用您的最佳判断来找到内存使用与批处理效率的良好折衷方案。例如,一种解决方案可能是为每个独特场景创建一个或两个纹理图集,或者更确切地说是“场景”,即使这意味着为另一个场景复制其他纹理图集中的一些图块。如果您的瓷砖几乎总是出现在任何风景中,那么将它们放在“共享”地图集中是有意义的。
至于子类化图块,我强烈支持避免子类化节点类。特别是如果对它们进行子类化的主要原因只是改变它们正在使用/动画的纹理。 sprite 已经是纹理的容器,因此您也可以更改 sprite 纹理并从外部对其进行动画处理。
要向节点添加数据或附加代码,您可以通过创建自己的 NSMutableDictionary 并向其添加所需的任何对象来细读其 userData 属性。典型的基于组件的方法如下:
SKSpriteNode* sprite = [SKSpriteNode spriteWithWhatever..];
[self addChild:sprite];
// create the controller object
sprite.userData = [NSMutableDictionary dictionary];
MyTileController* controller = [MyTileController controllerWithSprite:sprite];
[sprite.userData setObject: forKey:@"controller"];
然后,此控制器对象执行您的图块所需的任何自定义代码。它可以为瓷砖和其他任何东西设置动画。唯一重要的一点是将对拥有节点(此处为 sprite)的引用设为弱引用:
@interface MySpriteController
@property (weak) sprite; // weak is important to avoid retain cycle!
@end
因为精灵保留了字典。字典保留了控制器。如果控制器将保留精灵,则精灵无法解除分配,因为仍然会有对它的保留引用 - 因此它将继续保留保留控制器的字典,该控制器保留精灵。
使用基于组件的方法的优势(Kobold Kit 也支持和实现):
如果设计得当,可以与任何或多个节点一起使用。如果有一天您想要一个标签、效果、形状节点图块怎么办? 您不需要为每个图块创建一个子类。一些图块可能是简单的静态精灵。因此,请使用简单的静态 SKSpriteNode。 它允许您根据需要启动/停止或添加/删除各个方面。即使是在您最初不希望拥有或需要某个方面的瓷砖上。 组件允许您构建您将经常需要的功能库,甚至可能在其他项目中。 组件有助于构建更好的架构。一个经典的 OOP 设计错误是拥有 Player 和 Enemy 类,然后意识到两者都需要能够射箭和装备盔甲。所以你move the code to the root GameObject class,使代码对所有子类可用。使用组件,您只需将设备和射击组件添加到需要它的对象中。 基于组件的设计的最大好处是,您可以将各个方面与其他事物分开开发,因此可以根据需要重用和添加它们。您几乎可以自然而然地编写出更好的代码,因为您以不同的心态处理事情。 根据我自己的经验,一旦您将游戏模块化为组件,您会发现更少的错误,并且它们更容易解决,因为您不必查看或考虑其他组件的代码 - 除非组件使用但即使那么当一个组件触发另一个组件时,您有一个明确的边界,即当另一个组件接管时传递的值是否仍然正确?如果不是,则错误必须在第一个组件中。这是good introduction on component-based design。混合方法当然是要走的路。这里是more resources on component based design,但我强烈建议不要像“公认答案的作者”所建议的那样偏离路径并研究 FRP - FRP 是一个有趣的概念,但在游戏开发中还没有实际应用。
【讨论】:
总体上很好的详细答案。扩展关于 batching 的部分:如果你在 SKView 上将ignoresSiblingOrder
设置为 YES
,你不需要太担心孩子的顺序——Sprite Kit 会自动对它们进行排序用于高效绘图(例如,在一次绘图调用中来自同一纹理图集的所有精灵)。在图集上:考虑到ignoresSiblingOrder
,如果您的图集有太多/大的纹理,则可以使用节点层次结构将您的场景组织成“层”,每个层都可以一个draw call和一个atlas(例如world vs HUD)。
顺便说一句,这一行应该有一个对象:[sprite.userData setObject: forKey:@"controller"];
。应该是[sprite.userData setObject:controller forKey:@"controller"];
以上是关于SpriteKit 在哪里为数千个精灵加载纹理图集的主要内容,如果未能解决你的问题,请参考以下文章
如果我稍后使用 SKTextureAtlas,Sprite Kit 是不是会多次加载纹理图集?