前语

SpriteKit是为iOS和macOS开发的2D游戏引擎,由苹果开发和维护。SpriteKit能够让开发者在使用中方便地增加动画、物理作用、粒子作用等。但是在使用SpriteKit时往往会呈现CPU和内存占用高的问题,别的SpriteKit的坐标系和咱们开发常用的坐标系也不相同而且SpriteKit中播映GIF、Lottie、PAG等都是很难甚至无法完成的。在次以一个射击小游戏为例介绍一下优化过程

创立一个游戏场景

  1. 创立一个controller (SpriteKitMainViewController)并设置为根操控器
 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            let vc  = SpriteKitMainViewController()
            vc.view.frame = CGRect(x: 0, y: 0, width: SCREENW, height: SCREENH)
            window.rootViewController = vc
            self.window = window
            window.makeKeyAndVisible()
        }
    }
  1. 在SpriteKitMainViewController中创立SKMainScene(SKScene的子类)和SKView
import UIKit
import SpriteKit
class SpriteKitMainViewController: UIViewController {
    private lazy var skView :SKView = {
        let view = SKView(frame: CGRect(x: 0, y: 0, width: SCREENW, height: SCREENH))
        return view
    }()
    private lazy var scene :SKMainScene = {
        let scene = SKMainScene(size: CGSize(width: SCREENW, height: SCREENH))
        scene.scaleMode = .aspectFill
        return scene
    }()
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        setupUI()
    }
    private func setupUI(){
        self.view.addSubview(self.skView)
        //Create and configure the scene
        self.skView.presentScene(self.scene)
    }
}
  1. SKMainScene中创立物理场、背景、玩家人物、敌人人物,留意SpriteKit的描点默认是(0.5,0.5)而且坐标原点是在左下角和UIKit的不同

SpriteKit性能调优以及加载GIF、Lottie、PAG等动效资源。

 override init(size: CGSize) {
        super.init(size: size)
        initPhysicsWorld()
        initBackground()
        initplayerRole()
        initFoeRole()
    }
private func initplayerRole() {
        let image = OCTool.image(toTransparent: "bear", deleteWhite: false)
        playerRole = SKSpriteNode(texture: SKTexture(image: image))
        playerRole?.position = CGPoint(x: 160, y: 50)
        playerRole?.size = CGSize(width: 100, height: 100)
        playerRole?.zPosition = 1
        playerRole?.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: (playerRole?.size.width ?? 0)-20, height: playerRole?.size.height ?? 0))
        playerRole?.physicsBody?.categoryBitMask = SKRoleCategory.SKRoleCategoryplayerRole.rawValue
        playerRole?.physicsBody?.collisionBitMask = SKRoleCategory.SKRoleCategoryFoeBullet.rawValue//0
        playerRole?.physicsBody?.contactTestBitMask = SKRoleCategory.SKRoleCategoryFoeBullet.rawValue//UInt32.init(SKRoleCategory.SKRoleCategoryplayerRole.rawValue)
        addChild(playerRole!)
       firingBullets(bulletType: 0)
    }
 private func initFoeRole() {
         let image = UIImage(named: "monster")
        guard let image = image else {return}
        foeRole = SKSpriteNode(texture: SKTexture(image: image))
        guard let foeRole = foeRole else {return}
        foeRole.name = "foePlane"
        foeRole.size = CGSize(width: image.size.width*2, height: image.size.height*2)
        foeRole.physicsBody?.isDynamic = true
        //categoryBitMask  设置物理体标识符
        foeRole.physicsBody?.categoryBitMask = SKRoleCategory.SKRoleCategoryFoePlayer.rawValue
        //collisionBitMask  设置磕碰标识符  (非零 决定了物体能否磕碰反应)
        foeRole.physicsBody?.collisionBitMask = SKRoleCategory.SKRoleCategoryplayerRole.rawValue
        //contactTestBitMask 设置能够那类物理体磕碰  ->触发检测磕碰事情
        foeRole.physicsBody?.contactTestBitMask = SKRoleCategory.SKRoleCategoryplayerRole.rawValue
        foeRole.position = CGPoint(x: self.size.width*0.5, y: self.size.height - APPTool.topSafeAreaMargin - foeRole.size.height*0.5)
        self.addChild(foeRole)
        foeRoleAction()
        firingBullets(bulletType: 1)
     }

这三个特点是 SpriteKit 结构中 SKPhysicsBody 类的成员特点,用于界说物理体之间的磕碰和触摸测验。

  1. categoryBitMask:该特点指定了物理体所属的分类,用一个位掩码(bitmask)来表示。物理体能够归于多个分类,假如两个物理体的分类位掩码相同,则它们归于同一类,将会相互影响。
  2. collisionBitMask:该特点指定了当时物理体能够产生磕碰的分类。假如两个物理体的分类位掩码的交集不为空,则它们将会产生磕碰。
  3. contactTestBitMask:该特点指定了当时物理体需要检测触摸事情的分类。假如两个物理体的分类位掩码的交集不为空,则它们将会触发触摸事情。

例如,假定要让一个小球能够弹跳,而且能够与墙壁产生磕碰。能够把小球的 categoryBitMask 设置为 0b001,把墙壁的 categoryBitMask 设置为 0b010,然后将小球的 collisionBitMask 设置为 0b010,这样小球就只会和墙壁产生磕碰,而不会和其他障碍物产生磕碰。

总归,这三个特点能够帮助开发者操控物理体之间的磕碰和触摸测验,完成各种有趣的物理作用,如弹跳、冲突、磕碰等。

  1. 玩家移动躲避子弹或走位发射子弹在SKMainScene中完成系统的touchesMoved办法
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            var location = touch.location(in: self)
            guard let playerRole = playerRole else {return}
            // 超出屏幕
            if location.x >= self.size.width - (playerRole.size.width / 2) {
                location.x = self.size.width - (playerRole.size.width / 2)
            } else if location.x <= (playerRole.size.width / 2) {
                location.x = playerRole.size.width / 2
            }
            if location.y >= self.size.height - (playerRole.size.height / 2) {
                location.y = self.size.height - (playerRole.size.height / 2)
            } else if location.y <= (playerRole.size.height / 2) {
                location.y = (playerRole.size.height / 2)
            }
            let action = SKAction.move(to: CGPoint(x: location.x, y: location.y), duration: 0.1)
            playerRole.run(action)
        }
    }
  1. 磕碰检测,主要检测敌方子弹和己方子弹磕碰时互相抵消,敌方子弹击中己方时敌方子弹消失(己方子弹击中敌人时作用未完善)
 func didBegin(_ contact: SKPhysicsContact) {
        if (contact.bodyA.categoryBitMask & SKRoleCategory.SKRoleCategoryFoeBullet.rawValue != 0 || contact.bodyB.categoryBitMask & SKRoleCategory.SKRoleCategoryFoeBullet.rawValue != 0) && (contact.bodyA.categoryBitMask & SKRoleCategory.SKRoleCategoryPlayerBullet.rawValue != 0 || contact.bodyB.categoryBitMask & SKRoleCategory.SKRoleCategoryPlayerBullet.rawValue != 0){
            contact.bodyA.node?.removeFromParent()
            contact.bodyB.node?.removeFromParent()
        } else if (contact.bodyA.categoryBitMask & SKRoleCategory.SKRoleCategoryFoeBullet.rawValue != 0 || contact.bodyB.categoryBitMask & SKRoleCategory.SKRoleCategoryFoeBullet.rawValue != 0) && (contact.bodyA.categoryBitMask & SKRoleCategory.SKRoleCategoryplayerRole.rawValue != 0 || contact.bodyB.categoryBitMask & SKRoleCategory.SKRoleCategoryplayerRole.rawValue != 0) {
            if contact.bodyA.categoryBitMask == SKRoleCategory.SKRoleCategoryFoeBullet.rawValue {
                contact.bodyA.node?.removeFromParent()
            }else if contact.bodyB.categoryBitMask == SKRoleCategory.SKRoleCategoryFoeBullet.rawValue {
                contact.bodyB.node?.removeFromParent()
            }
        }
        }

至此一个简略的射击小游戏完成

改善

问题

问题1:CPU和内存耗费大

看一下CPU的耗费如下图

SpriteKit性能调优以及加载GIF、Lottie、PAG等动效资源。

目前界面主要是敌我两边和射击的子弹以及背景图CPU现已到达37%,为此是否有办法对CPU的使用率进行优化?

问题2:不能播映GIF等作用

别的尽管SKSpriteNode能够通过SKAction.repeatForever办法播映多张图片到达类似播映GIF的作用

 let animationAction = SKAction.repeatForever(SKAction.animate(with: textureArray!, timePerFrame: 0.1))

需要留意的是:这种办法也具有一些明显的限制,比方无法像原生的GIF动画一样循环播映而且无法操控播映速度,同时也不能以任意的分辨率缩放GIF动画。当遇到播映lottie设置是pag格局的文件时更加显得力不从心。

思路

加载SKView造成了很多的CUP这儿用自界说的View替代SKViwe烘托到屏幕上然后到达下降CPU的开销,别的通过重写func update(_ currentTime: TimeInterval)能够获得SKView每一帧的回调,假如选用UIView替代SKView上的元素SKNode在update函数中监听SKNode的方位巨细等改变去更新UIView,那么在UIView上通过增加子控件就能够完成播映GIF、pag、lottie等文件

下降CPU和内存使用

替代SKView

  1. SpriteKitMainViewController中选用自界说的UIView(scene.contentView)替代skView
     private func setupUI(){
        //self.scene.contentView替代self.skView
        //self.view.addSubview(self.skView)
        self.view.addSubview(self.scene.contentView)
        //Create and configure the scene
        self.skView.presentScene(self.scene)
        scene.setUpUI()
    }

其间scene.contentView 为自界说UIView

SKMainScene中的完成

  private(set) lazy var contentView: SceneView = {
        let view = SceneView()
        view.touchesMovedClosure = {
            [weak self](touchs,event)  in
            guard let self = self else {
                return
            }
            self.touchesMoved(touchs, with: event)
        }
        return view
    }()
    class SceneView:UIView{
}

自界说AJSpriteNode替代本来SKSpriteNode并完成界面显现

  1. 自界说AJSPriteNode而且在AJSpriteNode中界说特点contentView
class AJSpriteNode: SKSpriteNode {
    private(set)  var contentView :NodeContentView? = NodeContentView()
    func removeContentView(){
        self.contentView?.removeFromSuperview()
        contentView = nil
    }
}

其间contentView是实践加到界面替代SKSpriteNode显现的视图,removeContentView办法是当子弹击中方针或许飞出屏幕时分移除的办法

  1. AJSpriteNode的contentView替代SKSpriteNode在界面显现,以用户玩家为例
 private func initplayerRole() {
        playerRole = AJSpriteNode()
       guard let playerRole = playerRole else {return}
        playerRole.position = CGPoint(x: 160, y: 50)
        playerRole.size = CGSize(width: 100, height: 100)
       //setUpNodeContent办法完成contentView增加到SceneView上并完成坐标转化
       setUpNodeContent(node: playerRole, imgStr: "bear")
        playerRole.zPosition = 1
       playerRole.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: (playerRole.size.width )-20, height: playerRole.size.height ))
        playerRole.physicsBody?.categoryBitMask = SKRoleCategory.SKRoleCategoryplayerRole.rawValue
        playerRole.physicsBody?.collisionBitMask = SKRoleCategory.SKRoleCategoryFoeBullet.rawValue
        playerRole.physicsBody?.contactTestBitMask = SKRoleCategory.SKRoleCategoryFoeBullet.rawValue
       addChild(playerRole)
       firingBullets(bulletType: 0)
    }

setUpNodeContent的详细完成

private func setUpNodeContent(node:AJSpriteNode,imgStr:String?){
        if let imgStr = imgStr{
            //这儿是考虑到子弹是根据色彩生成所以不走这个创立UIImage的办法
            node.contentView?.imgView.image =  OCTool.image(toTransparent: imgStr, deleteWhite: false)
        }
        //把SKSpriteNode地点的SpriteKit坐标转化成UIKit的坐标系统
        let point = view?.convert(node.position, from: self) ?? .zero
        node.contentView?.frame = CGRect(x: 0, y: 0, width: node.size.width, height: node.size.height)
        //这儿设置centent是由于SKSpriteNode的锚点是(0.5,0.5)
        node.contentView?.AJCenterY = point.y
        node.contentView?.AJCenterX = point.x
        if let nodeContentView = node.contentView{
            contentView.addSubview(nodeContentView)
        }
        //加入到数组是为了后续方便取值
        nodeArray.append(node)
    }

玩家移动的完成修正

由于此刻的SKView没有增加到界面(addSubView)所以原SKMainScene中的touchesMoved办法不会被执行,此刻在屏幕移动触发的是增加到SpriteKitMainViewController.view的SceneView touchesMoved办法,为此这儿把SceneView的touchesMoved回调到 SKMainScene上保存本来的逻辑

  1. SceneView touchesMoved完成和转发。实践上这儿只是单纯的转发
class SceneView:UIView{
    var touchesMovedClosure:((_ touches: Set<UITouch>,_ event: UIEvent?)->Void)?
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?){
        //通过闭包回调到SKMainScene然后调用SKMainScene的touchesMoved办法
        self.touchesMovedClosure?(touches, event)
    }
}
//在SKMainScene中
 private(set) lazy var contentView: SceneView = {
        let view = SceneView()
        view.touchesMovedClosure = {
            [weak self](touchs,event)  in
            guard let self = self else {
                return
            }
            self.touchesMoved(touchs, with: event)
        }
        return view
    }()
  1. 这儿相对本来的办法增加了坐标转化
 override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            var location = touch.location(in: self)
            guard let playerRole = playerRole else {return}
            //坐标系的转化
            location = convert(location, to: self)
            // 超出屏幕
            if location.x >= self.size.width - (playerRole.size.width / 2) {
                location.x = self.size.width - (playerRole.size.width / 2)
            } else if location.x <= (playerRole.size.width / 2) {
                location.x = playerRole.size.width / 2
            }
            if location.y >= self.size.height - (playerRole.size.height / 2) {
                location.y = self.size.height - (playerRole.size.height / 2)
            } else if location.y <= (playerRole.size.height / 2) {
                location.y = (playerRole.size.height / 2)
            }
            //这儿的X、Y 不需要考虑锚点问题,由于会在update(_ currentTime: TimeInterval)中校正
            let action = SKAction.move(to: CGPoint(x: location.x, y: location.y), duration: 0.1)
            playerRole.run(action)
        }
    }

实时更新个Node元素的Frame

这儿按实践出发由于这儿的巨细不会产生改变所以这儿只更新Node的方位坐标,不更新巨细

  1. 重写SKMainScene的func update(_ currentTime: TimeInterval)办法
 override func update(_ currentTime: TimeInterval) {
        let nodes = self.nodeArray
        for node in nodes{
            if let node = node as? AJSpriteNode {
                if node.contentView?.superview == nil,let contentView = node.contentView{
                    //避免在调用该办法时node.contentView未增加到SceneView上
                    self.contentView.addSubview(contentView)
                }
                let positon = self.convertPoint(toView: node.position)
                node.contentView?.AJCenterX = positon.x
                node.contentView?.AJCenterY = positon.y
            }
        }
    }

至此,完成了SpriteKit元素到UIKit元素的替换,运转项目看看CPU和内存状况

数据比照

停止时

SpriteKit性能调优以及加载GIF、Lottie、PAG等动效资源。

上图为修正前

SpriteKit性能调优以及加载GIF、Lottie、PAG等动效资源。

优化后

参数 修正前 优化后 优化比
CPU 37% 9% 75.7%
内存 65.8 28.7 56.6%

移动玩家时的参数

SpriteKit性能调优以及加载GIF、Lottie、PAG等动效资源。

SpriteKit性能调优以及加载GIF、Lottie、PAG等动效资源。

参数 修正前 优化后 优化比
CPU 41% 17% 59%
内存 65.5 28.8 56%

作用

暂时无法在飞书文档外展示此内容

播映GIF、PAG、lottie等资源

由于咱们现在在界面上现已用UIView替代了SKNode所以播映上述资源的方案和咱们平时的使用办法是共同的,这儿以播映GIF为例

NodeContentView的修正

  1. 在NodeContentView中增加FLAnimatedImageView
 private var gifView :FLAnimatedImageView?
  func setupGIF(){
        gifView = FLAnimatedImageView()
        gifView?.isUserInteractionEnabled = true
        gifView?.isHidden = true
        gifView?.animationRepeatCount = 0
        gifView?.contentMode = .scaleAspectFill
        addSubview(gifView!)
        self.setNeedsLayout()
        self.layoutIfNeeded()
    }
  1. 露出播映GIF的办法
    func playGIf(){
        imgView.isHidden = true
        gifView?.isHidden = false
        let path = Bundle.main.path(forResource: "bear", ofType: "gif")
        let data = NSData(contentsOfFile: path ?? "") as? Data
        guard let data = data else {return}
        gifView?.animatedImage = FLAnimatedImage(animatedGIFData: data)
        gifView?.startAnimating()
    }
  1. 点击图像(小熊)时停止播映并回调
@objc private func stopGif(){
        gifView?.isHidden = true
        gifView?.stopAnimating()
        imgView.isHidden = false
        self.stopClosure?()
    }

AJSpriteNode的修正

  1. 增加hitCount核算当小熊中弹3次时触发播映GIF

privatevar hitCount = 0

  1. 传入标识创立GIF,这儿由于是demo所以用Mark == 100 代表小熊,才创立GIF。其他如敌机或许子弹等不创立实践开发中按实践状况处理
  //当Mark == 100是创立GIF
    var mark:Int8 = 0{
        didSet{
            if mark == 100{
                //玩家的mark为100
                contentView?.stopClosure = {
                //点击小熊的回调清空计数
                    [weak self] in
                    guard let self = self else {
                        return
                    }
                    self.hitCount = 0
                }
                contentView?.setupGIF()
            }
        }
    }
  1. 触发中弹的处理,中弹3次后触发GIF
 //中弹的处理
    func getshot(){
        if hitCount < 3{
            hitCount += 1
            return
        }
        contentView?.playGIf()
    }

作用:

SpriteKit性能调优以及加载GIF、Lottie、PAG等动效资源。

demo地址:gitee.com/liangaijun/…

结语

关于SpriteKit的其他优化方案网上也有介绍这儿就不逐个列出,假如文章长有呈现纰漏期望我们指出