Низкая производительность с SKShapeNode в Sprite Kit
Я делаю клон " Achtung die kurve " в Sprite Kit. Для постоянно движущихся линий / игроков я использую CGMutablePathRef вместе с SKShapeNode. В методе обновления я делаю это
// _lineNode is an instance of SKShapeNode and path is CGMutablePathRef
CGPathAddLineToPoint(path, NULL, _xPos, _yPos);
_lineNode.path = path;
добавить в строку. Метод update также постоянно обновляет _xPos и _yPos, чтобы он рос.
Я предполагаю, что я действительно спрашиваю, есть ли другой, более эффективный способ рисования линий, так как способ, которым я это делаю, теперь слишком сильно снижает частоту кадров через некоторое время (около 15-20 секунд). В этот момент FPS постоянно падает до тех пор, пока игра не станет недоступной для игры. Time Profiler сообщает мне, что эта строка: _lineNode.path = path является причиной сброса FPS.
Спасибо за любую помощь! Это очень ценится.
PS. Я пытаюсь вообще не использовать SKShapeNode, так как кажется, что они не могут хорошо рисовать линии (маленькие дырки / артефакты на кривых и т. Д.)
Скриншот:
3 ответа
К сожалению, SKShapeNode не так хорош для того, что вы пытаетесь сделать. Тем не менее, есть способ оптимизировать это, хотя и с некоторыми оговорками.
Во-первых, одна из самых больших проблем с fps заключается в том, что число ничьих становится чрезвычайно высоким, потому что каждый добавляемый вами отрезок линии - это еще одна ничья. Если вы установите showsDrawCount
на ваше SKView
Например, вы поймете, что я имею в виду.
В этом ответе несколько скшапеноде в одном розыгрыше?, вы можете получить больше информации о том, как вы можете использовать shouldRasterize
свойство SKEffectNode
решить проблему, если вы рисуете что-то один раз. Если вы этого не сделаете, вы будете тратить процессорное время на многочисленные розыгрыши каждого кадра.
Таким образом, вы можете видеть, что ничья - это главная проблема, когда вы не получаете желаемую производительность. Тем не менее, вы, кажется, хотите постоянно рисовать, поэтому то, что я собираюсь предложить, может оказаться для вас жизнеспособным решением.
Логика решения, которое я предлагаю, такова:
1 - Создать SKSpriteNode
что мы можем использовать в качестве холста.
2 - Создать SKShapeNode
это будет использоваться для рисования ТОЛЬКО текущего сегмента линии.
3 - Сделать это SKShapeNode
дитя холста.
4 - Нарисуйте новый отрезок линии через SKShapeNode
5 - Используйте SKView
метод `textureFromNode, чтобы сохранить то, что в данный момент нарисовано на холсте.
6 - установить текстуру холста на эту текстуру.
Вернитесь к #4 и проложите новый путь для вашего SKShapeNode
для следующего отрезка.
Повторите по мере необходимости.
Результатом должно быть то, что ваш счет ничьей никогда не будет больше 2 ничьих, что решило бы проблему большого количества ничьих.
По сути, вы сохраняете то, что ранее было нарисовано в текстуре, поэтому вам нужна только одна SKShapeNode
ничья для последнего отрезка и одна ничья для SKTexture
,
Опять же, я еще не пробовал этот процесс, и если бы было какое-то отставание, это было бы в этом textureFromNode
Назовите каждый кадр. Если что-нибудь и станет вашим узким местом, так оно и будет!
Я мог бы попробовать эту теорию сегодня, когда мне нужно textureFromNode
для другой проблемы, которую я пытаюсь решить, и поэтому я определенно выясню, насколько быстрый / медленный этот метод! ха-ха
ОБНОВИТЬ
Это не полный код, но важная часть для достижения желаемой производительности при рисовании (60 кадров в секунду):
Основные элементы узла:
container -> SKNode, который содержит все элементы, которые необходимо кэшировать
canvas -> SKSpriteNode, который будет отображать кэшированную версию нарисованных сегментов
пул сегментов -> используется для начального рисования сегментов и используется повторно при необходимости
Сначала создайте пул SKShapeNodes:
pool = [[NSMutableArray alloc]init];
//populate the SKShapeNode pool
// the amount of segments in pool, dictates how many segments
// will be drawn before caching occurs.
for (int index = 0; index < 5; index++)
{
SKShapeNode *segment = [[SKShapeNode alloc]init];
segment.strokeColor = [SKColor whiteColor];
segment.glowWidth = 1;
[pool addObject:segment];
}
Затем создайте метод для получения SKShapeNode из пула:
-(SKShapeNode *)getShapeNode
{
if (pool.count == 0)
{
// if pool is empty,
// cache the current segment draws and return segments to pool
[self cacheSegments];
}
SKShapeNode *segment = pool[0];
[pool removeObjectAtIndex:0];
return segment;
}
Затем создайте метод для получения сегмента из пула и рисования линии:
-(void)drawSegmentFromPoint:(CGPoint)fromPoint toPoint:(CGPoint)toPoint
{
SKShapeNode *curSegment = [self getShapeNode];
CGMutablePathRef path = CGPathCreateMutable();
curSegment.lineWidth = 3;
curSegment.strokeColor = [SKColor whiteColor];
curSegment.glowWidth = 1;
curSegment.name = @"segment";
CGPathMoveToPoint(path, NULL, fromPoint.x, fromPoint.y);
CGPathAddLineToPoint(path, NULL, toPoint.x, toPoint.y);
curSegment.path = path;
lastPoint = toPoint;
[canvas addChild:curSegment];
}
Далее следует метод создания текстуры и возврата существующих сегментов в пул:
-(void)cacheSegments
{
SKTexture *cacheTexture =[ self.view textureFromNode:container];
canvas.texture = cacheTexture;
[canvas setSize:CGSizeMake(canvas.texture.size.width, canvas.texture.size.height)];
canvas.anchorPoint = CGPointMake(0, 0);
[canvas enumerateChildNodesWithName:@"segment" usingBlock:^(SKNode *node, BOOL *stop)
{
[node removeFromParent];
[pool addObject:node];
}];
}
Наконец сенсорные обработчики:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self cacheSegments];
for (UITouch *touch in touches)
{
CGPoint location = [touch locationInNode:self];
lastPoint = location;
[self drawSegmentFromPoint:lastPoint toPoint:location];
}
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches)
{
CGPoint location = [touch locationInNode:self];
[self drawSegmentFromPoint:lastPoint toPoint:location];
}
}
Как я уже сказал, это не полный код, я предполагаю, что вы достаточно понимаете концепцию, которую вы можете внедрить в свое приложение. Это всего лишь примеры моей базовой реализации.
Чтобы исправить "дыры" в кривых, просто установите lineCap в ненулевое значение:
curSegment.lineCap = 1;
Я пытаюсь перевести на Swift 2.2 хороший ответ, предложенный прототипом:
Пользовательский узел:
class AOShapeNode: SKNode {
var currentScene: SKScene!
var canvas = SKShapeNode()
var segment = SKShapeNode()
var pool:[SKShapeNode]!
var poolSize: Int = 50 // big number to improve performance
var segmentLineWidth: CGFloat = 3
init(currentScene scene:SKScene,nodeSize size: CGSize) {
super.init()
print("---");
print("∙ \(self.dynamicType)")
print("---")
self.userInteractionEnabled = true
self.currentScene = scene
self.addChild(canvas)
pool = [SKShapeNode]()
for _ in 0..<poolSize
{
let segment = SKShapeNode()
segment.strokeColor = UIColor.blackColor()
segment.glowWidth = 1
segment.lineCap = CGLineCap(rawValue: 1)!
pool.append(segment)
}
}
func getShapeNode() -> SKShapeNode {
if(pool.count == 0)
{
self.cacheSegments()
}
let segment = pool.first
pool.removeFirst()
return segment!
}
func drawSegmentFromPoint(fromPoint:CGPoint, toPoint:CGPoint)->CGPoint {
let curSegment = self.getShapeNode()
let path = CGPathCreateMutable()
curSegment.lineWidth = segmentLineWidth
curSegment.strokeColor = SKColor.blackColor()
curSegment.glowWidth = 1
curSegment.lineCap = CGLineCap(rawValue: 1)!
curSegment.name = "segment"
CGPathMoveToPoint(path, nil, fromPoint.x, fromPoint.y)
CGPathAddLineToPoint(path, nil, toPoint.x, toPoint.y)
curSegment.path = path
canvas.addChild(curSegment)
return toPoint
}
func cacheSegments() {
if let cacheTexture = self.currentScene.view?.textureFromNode(self) {
let resizeAction = SKAction.setTexture(cacheTexture, resize: true)
canvas.runAction(resizeAction)
}
canvas.enumerateChildNodesWithName("segment", usingBlock: {
(node: SKNode!, stop: UnsafeMutablePointer <ObjCBool>) -> Void in
self.pool.append(node as! SKShapeNode)
node.removeFromParent()
})
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Сцена игры:
class GameScene: SKScene {
var line : AOShapeNode!
var lastPoint :CGPoint = CGPointZero
override func didMoveToView(view: SKView) {
/* Setup your scene here */
line = AOShapeNode.init(currentScene: self, nodeSize: self.size)
self.addChild(line)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
line.cacheSegments()
for touch in touches {
let location = touch.locationInNode(self)
lastPoint = location
lastPoint = line.drawSegmentFromPoint(lastPoint, toPoint: location)
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch moved */
for touch in touches {
let location = touch.locationInNode(self)
lastPoint = line.drawSegmentFromPoint(lastPoint, toPoint: location)
}
}
}
Это выше, это весь код, но если вы хотите попробовать, это ссылка на репозиторий github.
Чтение некоторых статей о возможностях улучшения SKShapeNode
альтернатив или сильного ре-факторинга, я нашел этот проект под названием SKUShapeNode, фактически включенный в проект SKUtilities 2, идея сделать подкласс SKSpriteNode
что делает с использованием CAShapeLayer
( некоторая документация), есть некоторые ошибки, и всегда есть UIKit
CALayer
к актуальному Sprite Kit, используя узел.