前语
SpriteKit是为iOS和macOS开发的2D游戏引擎,由苹果开发和维护。SpriteKit能够让开发者在使用中方便地增加动画、物理作用、粒子作用等。但是在使用SpriteKit时往往会呈现CPU和内存占用高的问题,别的SpriteKit的坐标系和咱们开发常用的坐标系也不相同而且SpriteKit中播映GIF、Lottie、PAG等都是很难甚至无法完成的。在次以一个射击小游戏为例介绍一下优化过程
创立一个游戏场景
- 创立一个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()
}
}
- 在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)
}
}
- SKMainScene中创立物理场、背景、玩家人物、敌人人物,留意SpriteKit的描点默认是(0.5,0.5)而且坐标原点是在左下角和UIKit的不同
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 类的成员特点,用于界说物理体之间的磕碰和触摸测验。
categoryBitMask
:该特点指定了物理体所属的分类,用一个位掩码(bitmask)来表示。物理体能够归于多个分类,假如两个物理体的分类位掩码相同,则它们归于同一类,将会相互影响。collisionBitMask
:该特点指定了当时物理体能够产生磕碰的分类。假如两个物理体的分类位掩码的交集不为空,则它们将会产生磕碰。contactTestBitMask
:该特点指定了当时物理体需要检测触摸事情的分类。假如两个物理体的分类位掩码的交集不为空,则它们将会触发触摸事情。例如,假定要让一个小球能够弹跳,而且能够与墙壁产生磕碰。能够把小球的
categoryBitMask
设置为0b001
,把墙壁的categoryBitMask
设置为0b010
,然后将小球的collisionBitMask
设置为0b010
,这样小球就只会和墙壁产生磕碰,而不会和其他障碍物产生磕碰。总归,这三个特点能够帮助开发者操控物理体之间的磕碰和触摸测验,完成各种有趣的物理作用,如弹跳、冲突、磕碰等。
- 玩家移动躲避子弹或走位发射子弹在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)
}
}
- 磕碰检测,主要检测敌方子弹和己方子弹磕碰时互相抵消,敌方子弹击中己方时敌方子弹消失(己方子弹击中敌人时作用未完善)
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的耗费如下图
目前界面主要是敌我两边和射击的子弹以及背景图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
- 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并完成界面显现
- 自界说AJSPriteNode而且在AJSpriteNode中界说特点contentView
class AJSpriteNode: SKSpriteNode {
private(set) var contentView :NodeContentView? = NodeContentView()
func removeContentView(){
self.contentView?.removeFromSuperview()
contentView = nil
}
}
其间contentView是实践加到界面替代SKSpriteNode显现的视图,removeContentView办法是当子弹击中方针或许飞出屏幕时分移除的办法
- 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上保存本来的逻辑
- 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
}()
- 这儿相对本来的办法增加了坐标转化
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的方位坐标,不更新巨细
- 重写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和内存状况
数据比照
停止时
上图为修正前
优化后
参数 | 修正前 | 优化后 | 优化比 |
---|---|---|---|
CPU | 37% | 9% | 75.7% |
内存 | 65.8 | 28.7 | 56.6% |
移动玩家时的参数
参数 | 修正前 | 优化后 | 优化比 |
---|---|---|---|
CPU | 41% | 17% | 59% |
内存 | 65.5 | 28.8 | 56% |
作用
暂时无法在飞书文档外展示此内容
播映GIF、PAG、lottie等资源
由于咱们现在在界面上现已用UIView替代了SKNode所以播映上述资源的方案和咱们平时的使用办法是共同的,这儿以播映GIF为例
NodeContentView的修正
- 在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()
}
- 露出播映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()
}
- 点击图像(小熊)时停止播映并回调
@objc private func stopGif(){
gifView?.isHidden = true
gifView?.stopAnimating()
imgView.isHidden = false
self.stopClosure?()
}
AJSpriteNode的修正
- 增加hitCount核算当小熊中弹3次时触发播映GIF
private
var
hitCount = 0
- 传入标识创立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()
}
}
}
- 触发中弹的处理,中弹3次后触发GIF
//中弹的处理
func getshot(){
if hitCount < 3{
hitCount += 1
return
}
contentView?.playGIf()
}
作用:
demo地址:gitee.com/liangaijun/…
结语
关于SpriteKit的其他优化方案网上也有介绍这儿就不逐个列出,假如文章长有呈现纰漏期望我们指出