SpriteKit在哪里加载数千个精灵的纹理图集?

8
在我的游戏中,有成千上万个“tile”节点组成了一个游戏地图(类似于模拟城市),我想知道每个节点纹理和动画的最高效率方式是什么?有几种独特的tile“类型”,每种类型都有自己的纹理图集/动画,因此确保在可能的情况下重用纹理非常关键。
所有的tile节点都是单个map节点的子节点,map节点应该处理识别tile类型和加载必要的图集和动画(例如通过从plist加载纹理和图集名称)吗?
或者,每种tile类型都是某种子类。每个SKSpriteNode tile是否最好处理自己的精灵图集加载,例如[tileInstance texturise];(Sprite Kit如何处理这个问题?这种方法会导致相同的纹理图集被加载到某个tile类型的每个实例中吗?)
我一直在查找有关图集和纹理重用的深入解释,但我不知道像这样的情况的典型程序是什么。感谢您的帮助。
1个回答

23

内存:首先,不会有任何明显的差异。您必须加载瓦片纹理,纹理将占地图+瓦片内存的至少99%。

纹理重用:纹理会自动被重用/缓存。使用相同纹理的两个精灵将引用同一纹理,而不是每个精灵都拥有自己的纹理副本。

帧率/批处理:这完全取决于适当地批处理。Sprite Kit通过按照它们添加到子节点数组中的顺序渲染它们来批处理节点的子项。只要下一个子节点使用与上一个子节点相同的纹理,它们就会全部批处理为一个绘制调用。可能最糟糕的事情是添加一个精灵、一个标签、一个精灵、一个标签等等。您需要尽可能多地添加使用连续顺序的相同纹理的精灵。

Atlas使用: 这里是你可以获得最多优势的地方。通常开发者会尝试对他们的图集进行分类,这是错误的做法。相反,你应该尽可能地创建尽可能少的纹理图集,每个图集包含尽可能多的瓷砖(和动画)。在所有iOS 7设备上,一个纹理图集可以是2048x2048的,除了iPhone 4和iPad 1之外,所有其他设备都可以使用高达4096x4096像素的纹理。

当然,也有例外情况,例如如果你有大量的纹理,无论如何都无法一次性将它们全部加载到所有设备的内存中。在这种情况下,你需要根据内存使用率和批处理效率找到一个好的折衷方案。例如,一个解决方案可能是为每个唯一的场景或“风景”创建一个或两个纹理图集,即使这意味着在另一个场景的其他纹理图集中重复某些瓷砖。如果你有几乎总是出现在任何场景中的瓷砖,将其放入“共享”图集中是有意义的。

关于子类化瓦片,我强烈建议避免子类化节点类。尤其是如果仅仅为了改变它们所使用/动画的纹理而进行子类化。精灵已经是一个纹理的容器,因此您可以从外部更改精灵纹理并对其进行动画处理。
要向节点添加数据或附加代码,可以通过创建自己的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类,然后意识到两者都需要能够射箭和装备护甲。所以您将代码移动到根GameObject类,使代码可用于所有子类。使用组件,您只需为需要它的对象添加一个装备和一个射击组件。
  • 组件化设计的巨大好处是,您可以将个别方面与其他事物分开开发,因此它们可以被重复使用并随时添加。您几乎自然地编写更好的代码,因为您用不同的思维方式来处理事物。
  • 从我的经验来看,一旦您将游戏模块化为组件,您将获得更少的错误,并且它们更容易解决,因为您不必查看或考虑其他组件的代码 - 除非由组件使用,但即使是这样,当一个组件触发另一个组件时,您有一个清晰的边界,例如当另一个组件接管时,传递的值是否仍然正确?如果不是,则错误必须在第一个组件中。

这是一个关于组件化设计的好引言。混合式方法肯定是正确的方式。这里有更多关于组件化设计的资源,但我强烈建议不要偏离正道并研究FRP,如“被接受答案的作者”所建议的 - FRP是一个有趣的概念,但在游戏开发中尚未有真正的应用。


5
总体而言,答案很详细。针对“批处理”部分进行扩展:如果在SKView上将ignoresSiblingOrder设置为YES,则无需过于担心子节点的顺序 - Sprite Kit会自动进行排序以实现高效绘制(例如,同一纹理集中的所有精灵在一个绘制调用中)。在纹理集方面,考虑到ignoresSiblingOrder,如果有太多/太大的纹理需要放入一个纹理集中,可以使用节点层次结构将场景组织成“图层”,每个图层可以是一个绘制调用和一个纹理集(例如,世界与HUD)。 - rickster
顺便说一下,这行代码应该有一个对象:[sprite.userData setObject: forKey:@"controller"];。应该是 [sprite.userData setObject:controller forKey:@"controller"]; - rizzes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接