Swift,SpriteKit:FPS低,更新方法中包含巨大的for循环

问题描述

使用以下代码在Sprite Kit中具有非常低的FPS(〜7fps至〜10fps)正常吗?

用例:

我只画了从下到上的线(1024 * 64线)。我有一些 delta 值,该值确定每一帧的单行位置。这些行代表我的CGPath,它每帧都分配给SKShapeNode。没有其他的。我想知道SpriteKit(或者也许是Swift)的性能。

您对提高性能有任何建议吗?

屏幕:

screenshot

代码:

import UIKit
import SpriteKit

class SKViewController: UIViewController {
    
    @IBOutlet weak var skView: SKView!
    
    var scene: SKScene!
    var lines: SKShapeNode!
    
    let N: Int = 1024 * 64
    var delta: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        scene = SKScene(size: skView.bounds.size)
        scene.delegate = self
        
        skView.showsFPS = true
        skView.showsDrawCount = true
        skView.presentScene(scene)
        
        lines = SKShapeNode()
        lines.lineWidth = 1
        lines.strokeColor = .white
        
        scene.addChild(lines)
    }
}

extension SKViewController: SKSceneDelegate {
    func update(_ currentTime: TimeInterval,for scene: SKScene) {
        let w: CGFloat = scene.size.width
        let offset: CGFloat = w / CGFloat(N)
        
        let path = UIBezierPath()
        
        for i in 0 ..< N { // N -> 1024 * 64 -> 65536
            let x1: CGFloat = CGFloat(i) * offset
            let x2: CGFloat = x1
            let y1: CGFloat = 0
            let y2: CGFloat = CGFloat(delta)

            path.move(to: CGPoint(x: x1,y: y1))
            path.addLine(to: CGPoint(x: x2,y: y2))
        }
        
        lines.path = path.cgPath
        
        // Updating delta to simulate the changes
        //
        if delta > 100 {
            delta = 0
        }
        delta += 1
    }
}

感谢和问候, 相叶^ _ ^

解决方法

CPU

65536是一个相当大的数字。告诉CPU理解这么多循环总是会导致运行缓慢。例如,即使我制作了一个仅测试运行空循环所需时间的测试命令行项目:

while true {
    let date = Date().timeIntervalSince1970


    for _ in 1...65536 {}

    let date2 = Date().timeIntervalSince1970
    print(1 / (date2 - date))
}

这将导致〜17 fps。我什至没有应用CGPath,它已经非常慢了。


调度队列

如果您想将游戏保持在60fps的速度,但是特定于CGPath的渲染可能仍然很慢,则可以使用DispatchQueue

var rendering: Bool = false // remember to make this one an instance value

while true {
    let date = Date().timeIntervalSince1970

    if !rendering {
        rendering = true
        let foo = DispatchQueue(label: "Run The Loop")
        foo.async {
            for _ in 1...65536 {}
            let date2 = Date().timeIntervalSince1970
            print("Render",1 / (date2 - date))
        }
        rendering = false
    }
}

这保留了自然的60fps体验,您可以更新其他对象,但是,SKShapeNode对象的呈现仍然很慢。


GPU

如果您想加快渲染速度,我建议您考虑在GPU而不是CPU上运行它。 GPU(图形处理单元)更适合于此,并且可以处理巨大的循环而不会干扰游戏玩法。这可能需要您将其编程为SKShader,其中包含教程。

,

检查细分数量

没有iOS设备的屏幕宽度超过3000像素或1500点(视网膜屏幕具有逻辑和物理像素,其中点等于2或3像素,具体取决于在比例因子上; iOS可以使用点,但您还必须记住像素),并且甚至最接近的是在横向模式下具有最大屏幕(iPad Pro 12.9和iPhone Pro Max)的屏幕。

纵向的典型设备会少于500点,宽度为1500像素。

您将该宽度划分为65536个部分,最终将得到 pixel (不是偶数点)坐标,例如0.00、0.05、0.10、0.15,...,0.85,这实际上是指到同一像素二十倍(我的结果在iPhone模拟器中四舍五入)。

您的代码在完全相同的物理位置上画了二十到六十条线,彼此重叠!为什么这样如果将N设置为w并为offset使用1.0,则在60 FPS时将具有相同的可见结果。

重新考虑方法

即使您大大减少了每帧要完成的工作量,该实现仍然会有一些缺点。不建议在update(_:)中推进动画帧,因为您无法在FPS上获得保证,并且通常希望动画遵循设定的时间表,即在1秒内完成而不是60帧。 (如果FPS降至10,则60帧的动画将在6秒内完成,而1秒钟的动画仍将在1秒内完成,但是帧速率较低,即跳过帧。)

可见,动画的作用是在屏幕上绘制一个矩形,该矩形的宽度会填充整个屏幕,并且其高度会从0点增加到100点。我会说,实现此目标的一种更“标准”的方法如下:

let sprite = SKSpriteNode(color: .white,size: CGSize(width: scene.size.width,height: 100.0))
sprite.yScale = 0.0
scene.addChild(sprite)
sprite.run(SKAction.repeatForever(SKAction.sequence([
    SKAction.scaleY(to: 1.0,duration: 2),SKAction.scaleY(to: 0.0,duration: 0.0)
])))

请注意,我使用SKSpriteNode是因为据说SKShapeNode存在bug和性能问题,尽管过去几年人们报告了一些改进。

但是,如果您确实由于某些特定需求而坚持在每帧重新绘制精灵的整个纹理,那么对于自定义着色器来说确实是一件好事……但是这些需要学习一种全新的方法,更不用说一种新的编程语言了。

您的着色器将在GPU上针对每个像素执行。我再说一遍:该代码将针对每个单个像素执行-与SpriteKit世界完全不同。

着色器将访问一堆值以供使用,例如一组标准化的坐标(在v_tex_coord变量中的(0.0,0.0)和(1.0,1.0)之间)和系统时间(秒) (因为着色器一直在运行)({{1}),因此它需要确定相关像素需要为哪个颜色值–并将其存储在变量u_time中进行设置。

可能是这样的:

gl_FragColor

将其放入名为void main() { // default to a black color,or a three-dimensional vector v3(0.0,0.0,0.0): vec3 color = vec3(0.0); // take the fraction part of the time in seconds; // this value will go from 0.0 to 0.9999… every second,then drop back to 0.0. // use this to determine the relative height of the area we want to paint white: float height = fract(u_time); // check if the current pixel is below the height of the white area: if (v_tex_coord.y < height) { // if so,set it to white (a three-dimensional vector v3(1.0,1.0,1.0)): color = vec3(1.0); } gl_FragColor = vec4(color,1.0); // the fourth dimension is the alpha } 的文件中,创建全屏精灵shader.fsh,然后为其指定着色器:

mySprite

一旦显示了精灵,它的着色器将处理所有渲染。但是请注意,您的精灵会因此失去某些mySprite.shader = SKShader.init(fileNamed: "shader.fsh") 功能。

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...