This is for a puzzle game. As you can see from the screenshot, there are 256 nodes on screen and frame rate hovers around 10 FPS.
I see plenty of puzzle games out there with what I assume amounts to hundreds of separate nodes on screen, yet with great frame rates... I'd like to achieve the same. What are some optimization points I can consider based on my code? (Drawing code below)
I'm literally just creating a bunch of SKShapeNodes, putting them into an NSArray, and then adding them as children to the scene on didMoveToView
class GameScene: SKScene {
var grid: Grid! = nil
override func didMoveToView(view: SKView) {
self.grid = Grid()
self.grid.initWithParent(parent: self, rows:16, columns:8, hexagonSize:40.0)
self.grid.drawGrid()
}
override func update(currentTime: CFTimeInterval)
{
// nothing here!! why so slow..
}
}
//…
class Grid : NSObject {
// this all gets initialized later
var hexagonSize: CGFloat! = nil
var hexagonArray: NSMutableArray! = nil
var numRows: Int! = nil
var numColumns: Int! = nil
var parentScene: SKScene? = nil
// …
func drawGrid(){
//…
if(self.parentScene != nil){
for rowIdx in 0..<self.numRows {
for colIdx in 0..<self.numColumns {
//…
let hex = Hexagon()
hex.initWithParentNode(node: self.parentScene!, size: self.hexagonSize)
hex.drawAtPoint(positionPoint: hexCenter)
self.hexagonArray.addObject(hex) hex.drawAtPoint(:positionPoint)
}
}
}
}
func initWithParent(#parent: SKScene, rows: Int, columns: Int, hexagonSize: CGFloat){
self.parentScene = parent
self.numRows = rows
self.numColumns = columns
self.hexagonSize = hexagonSize
}
}
//…
class Hexagon: NSObject {
//…
var parentNode : SKNode? = nil
var size : CGFloat! = nil
var shape : SKShapeNode! = nil
func drawAtPoint(#positionPoint: CGPoint){
let diameter = self.size
let radius = diameter/2
let normal = radius * sqrt(3)/2
var path = CGPathCreateMutable()
self.shape = SKShapeNode(path: path)
let point = CGPointZero
CGPathMoveToPoint(path, nil, point.x, point.y+radius)
CGPathAddLineToPoint(path, nil, point.x+normal, point.y+(radius/2))
CGPathAddLineToPoint(path, nil, point.x+normal, point.y-(radius/2))
CGPathAddLineToPoint(path, nil, point.x, point.y-(diameter/2))
CGPathAddLineToPoint(path, nil, point.x-normal, point.y-(radius/2))
CGPathAddLineToPoint(path, nil, point.x-normal, point.y+(radius/2))
CGPathAddLineToPoint(path, nil, point.x, point.y+radius)
self.shape?.path = path
self.shape?.fillColor = SKColor.blueColor()
if(self.shape != nil && self.parentNode != nil){
self.shape.position = positionPoint
self.parentNode!.addChild(self.shape!)
}
}
func initWithParentNode(#node: SKNode, size: CGFloat){
self.parentNode = node
self.size = size
}
}
Well... you are manually drawing on the screen... You should leave to the SpriteKit engine the responsibility to draw your sprites on the screen (it has tons of optimizations to do this better then we can usually do).
My suggestions
First of all throw away your classes Grid and Hexagon.
Done? Good :)
The image
Get a software like iDraw
Draw an hexagon with transparent background
Then export it as hexagon.png (I prepared the image for you)
Add it to the Xcode Asset catalog
The Hexagon class
Create a file Hexagon.swift and write into it the following code
import SpriteKit
class Hexagon : SKSpriteNode {
init() {
let texture = SKTexture(imageNamed: "hexagon")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Populating the scene
Now it's just a matter of creating a few Hexagon(s) and placing them on your scene.
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let hexagon0 = Hexagon()
hexagon0.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
self.addChild(hexagon0)
// add more Hexagon(s) at different positions...
}
}
Conclusions
That's it. SpriteKit will take care of drawing the sprites you added to the scene.
Following this approach you can add a huge number of sprites on the screen, SpriteKit will do optimizations we mortal human beings will never know to keep the framerate as high as possible.
(The optimizations will be even better with Xcode 7 and iOS 9 since SpriteKit is now built on top of Metal).
Hope this helps.
Related
My question is
When i touch black arrow areas i still touch whole picture, but i want only touch and have action when i touch red area only.
I tried to give a name to spriteNode plane = childNode(withName: "play") but it take/touch all image frame not only alphaBody.
red and black areas
I did some searches on google but no positive results.
Many thanks.
a really simple solution could be to put hit test areas on your plane object (I've left them slightly red for the example but you would make them transparent). I created my plane class in the editor and dragged it as a reference into my scene, but you could easily just program the hit zones from scratch as well.
Then in your plane class add them to the plane and detect touches within the plane class itself. It's not exact but not that much farther off then your alpha tracing was in your picture.
A nice bonus of this method is that you can isolate this touches to areas of the plane. Some uses for that could be...
touch left/right wing to turn that direction
touch body to refuel
touch wings to change guns
here is the code I used in my Plane class
class Plane: SKSpriteNode {
private var background: SKSpriteNode!
private var wingsHitTest: SKSpriteNode!
private var bodyHitTest: SKSpriteNode!
private var smallWingsHitTest: SKSpriteNode!
init() {
super.init(texture: nil, color: .clear, size: CGSize.zero)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
isUserInteractionEnabled = true
self.color = .clear
if let background = self.childNode(withName: "//background") as? SKSpriteNode {
self.background = background
}
if let wingsHitTest = self.childNode(withName: "//wingsHitTest") as? SKSpriteNode {
self.wingsHitTest = wingsHitTest
}
if let bodyHitTest = self.childNode(withName: "//bodyHitTest") as? SKSpriteNode {
self.bodyHitTest = bodyHitTest
}
if let smallWingsHitTest = self.childNode(withName: "//smallWingsHitTest") as? SKSpriteNode {
self.smallWingsHitTest = smallWingsHitTest
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch? {
let touchLocation = touch.location(in: self)
if wingsHitTest.contains(touchLocation) {
print("hit in the wings")
}
else if smallWingsHitTest.contains(touchLocation) {
print("hit in the back wings")
}
else if bodyHitTest.contains(touchLocation) {
print("hit in the body")
}
}
}
}
It seems like the behavior you want is not included in SpriteKit so you will have to implement it yourself.
Quoting from the link above:
You will have to find out the locationInNode(spriteNode) of the UITouch object object to get the coordinates within the sprite and then read the alpha value from the sprite Image (not trivial) or precompute bitmasks (ones and zeros) from your images and read the value at the corresponding point in the bitmask for the sprite
So you will need to load your image as UIImage or CGImage and check whether the color at the pixels where the touch occurred was transparent or not. You can find more information on how to do that in this question: How to get pixel color from SKSpriteNode or from SKTexture?
I am coding a Bullet Hell type game for the IOS, which features multiple enemies coming down toward the player. For the enemies, I created a custom class called Enemy which is a subclass of the SKSpritenode class.
class Enemy : SKSpriteNode {
var type : String;
var health : Int;
var armor : Int;
var healthBar : HealthBar; //The HealthBar Class
init(type: String, position: CGPoint, texture : SKTexture, color: UIColor, size: CGSize){
self.type = type;
self.health = enemyHealths[self.type]!
self.armor = enemyArmors[self.type]!
self.healthBar = HealthBar(healthBarWidth: 40, healthBarHeight: 4, hostile: true, health: self.health, maxHealth: self.health, position: position) //Initialize HealthBar
super.init(texture: texture, color: color, size: size)
self.position = position;
self.physicsBody = SKPhysicsBody(rectangleOf: self.size);
self.physicsBody?.affectedByGravity = false;
self.physicsBody?.collisionBitMask = 0;
self.physicsBody?.isDynamic = true;
self.addChild(self.healthBar) //Add HealthBar Instance
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(currentTime: TimeInterval){
}
}
In this class I initialize another class, a HealthBar class, which is also a subclass of the SKSPritenode class, but when I try to add it as a child to the Enemy instance it shows up on the far right side of the screen. I am wondering if there are specific child position mechanics that are making this happen or of its just an error in the code. One thing I can think of might be causing this error is that when I update the HealthBar position I set its position to the same of the Enemy's, which is added as a child to the GameScene. This might cause an error with that coordinate not being available when the Healthbar is added as a child to the smaller Enemy.
As you can see the Enemy spawns just fine, but the HealthBar is on the far right of the screen instead of being right above the Enemy where its supposed to be.
Well, it turns out my suspicions were correct. Since you were adding a child to the smaller Enemy Instance, coordinate CGPoint(x: 0, y: 0) corresponded to the center of the Enemy Instance. Thus to get the HealthBar above the Enemy Instance, all you had to was set its position to the aforementioned coordinate with a small positive offset in the Y value.
Ok, I need to subclass SCNNode because I have different SCNNodes with different "abilities" in my game (I know people don't usually subclass SCNNode but I need to)
I have followed every other question like Subclassing SCNNode and Creating a subclass of a SCNNode
but continue to get this error:
fatal error: use of unimplemented initializer 'init()' for class 'LittleDude.Dude'
Where Dude is the name of my SCNNode subclass.
Following the second question, because of classing issues this is how I attempt to get the SCNNode from my .dae scene and assign it to my Dude():
var theDude = Dude(geometry: SCNSphere(radius: 0.1)) //random geometry first
var modelScene = SCNScene(named: "art.scnassets/ryderFinal3.dae")!
if let d = modelScene.rootNode.childNodes.first
{
theDude.transform = d.transform
theDude.geometry = d.geometry
theDude.rotation = d.rotation
theDude.position = d.position
theDude.boundingBox = d.boundingBox
theDude.geometry?.firstMaterial = d.geometry?.firstMaterial
}
print("DUDE: ", theDude)
Then in my Dude class:
class Dude: SCNNode {
init(geometry: SCNGeometry) {
super.init()
center(node: self)
self.scale = SCNVector3(x: modifier, y: modifier, z: modifier)
//theDude.transform = SCNMatrix4Mult(theDude.transform, SCNMatrix4MakeRotation(360, 0, 1, 0))
//theDude.worldOrientation = .
//self.theDude.position = SCNVector3Make(0, 0, -1)
for s in animScenes {
if let anim = animationFromSceneNamed(path: s)
{
animations.append(anim)
anim.usesSceneTimeBase = true
anim.repeatCount = Float.infinity
self.addAnimation(anim, forKey: anim.description)
}
}
}
}
/* Xcode required this */
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented222")
}
The error gets drawn on first line of this custom class and happens when I try to clone and add the custom SCNNode to my scene:
func makeDude(hitPosition: SCNVector3) {
//print("DUDE")
let clone = theDude.clone() as? SCNNode
clone?.position = hitPosition
self.sceneView.scene.rootNode.addChildNode(clone!)
}
Even though I cast to SCNNode to try to avoid an error. How can I just clone and use my custom SCNNode in my scene? What is wrong here?
Just to make clear, this answer is hidden on the comments of a previous answer, so to avoid the confusion here is the answer fully spelled out:
class NodeSubClass: SCNNode {
init(geometry: SCNGeometry?){
super.init()
self.geometry = geometry
}
...
}
If you subclass SCNNode and override its initializer init(geometry: SCNGeometry?) then you'll need to call the same initalizer of super during your init. Try changing
super.init()
to
super.init(geometry: geometry)
I am trying to develop a game which need to have a common background across all the scenes using SpriteKit and Swift. Since the background is common and its actions need to be continuous, I created a custom singleton subclass of SKSpriteNode like this:
class BackgroundNode: SKSpriteNode {
static let sharedBackground = BackgroundNode()
private init()
{
let texture = SKTexture(imageNamed: "Background")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
addActors()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func addActors() {
addClouds()
}
private func addClouds() {
addCloud1()
}
private func addCloud1() {
let cloud1 = SKSpriteNode(imageNamed: "Cloud1")
cloud1.size = CGSizeMake(0.41953125*self.size.width, 0.225*self.size.height)
cloud1.position = CGPointMake(-self.size.width/2, 0.7*self.size.height)
self.addChild(cloud1)
let moveAction = SKAction.moveTo(CGPointMake(self.size.width/2, 0.7*self.size.height), duration: 20)
cloud1.runAction(SKAction.repeatActionForever(moveAction))
}
}
And from the GameScene class I am adding this node to the current view like this:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
BackgroundNode.sharedBackground.size = CGSizeMake(self.size.width, self.size.height)
BackgroundNode.sharedBackground.position = CGPointMake(self.size.width/2, self.size.height/2)
addChild(BackgroundNode.sharedBackground)
}
}
The background image is showing up correctly, but the cloud is not getting added. As of the code above The cloud should appear out of the screen and animate into and then again out of the screen through the other edge, but to verify if it is getting added, I even tried adding the cloud to the center of the screen without any animations. Still the cloud didn't show up. What can be the issue here? And how to fix it?
EDIT
I figured out that the child is actually getting added but is getting added and moving through some points far above the screen. I also figured out that it MIGHT have something to do with anchor point of the cloud, but whatever value I set as anchor point, the cloud always remains on the top right corner of the screen. What can I do about the anchor points to make the clouds appear as it should(considering the lower left corner as (0, 0) is what I want)
Solved the issue. The problem was that I had to manually set the anchor points of the Scene and the node. Setting the anchor point of both the Scene and the node to (0, 0) solved the issue. The new code looks as follows:
GameScene
override func didMoveToView(view: SKView) {
anchorPoint = CGPointMake(0, 0) //Added this
BackgroundNode.sharedBackground.size = CGSizeMake(self.size.width, self.size.height)
BackgroundNode.sharedBackground.position = CGPointMake(0, 0)
addChild(BackgroundNode.sharedBackground)
}
BackgroundNode
private init()
{
let texture = SKTexture(imageNamed: "Background")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
anchorPoint = CGPointMake(0, 0) //Added this
addActors()
}
How can I add my starsSqArray to a for loop in my update function that grabs all the SKSpriteNodesfor _starsSq1 so that all of the stars move together and not separately?
Right now my Swift class keeps returning an error saying that _starsSqArray doesn't have a position (my code is bugged out). My goal is to grab the plotted stars and move them downward all at once.
import SpriteKit
class Stars:SKNode {
//Images
var _starsSq1:SKSpriteNode?
//Variables
var starSqArray = Array<SKSpriteNode>()
var _starSpeed1:Float = 5;
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init() {
super.init()
println("stars plotted")
createStarPlot()
}
/*-------------------------------------
## MARK: Update
-------------------------------------*/
func update() {
for (var i = 0; i < starSqArray.count; i++) {
_starsSq1!.position = CGPoint(x: self.position.x , y: self.position.y + CGFloat(_starSpeed1))
}
}
/*-------------------------------------
## MARK: Create Star Plot
-------------------------------------*/
func createStarPlot() {
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
for (var i = 0; i < 150 ; i++) {
_starsSq1 = SKSpriteNode(imageNamed: "starSq1")
addChild(_starsSq1!)
//starSqArray.addChild(_starsSq1)
var x = arc4random_uniform(UInt32(screenSize.width + 400))
var y = arc4random_uniform(UInt32(screenSize.height + 400))
_starsSq1!.position = CGPoint(x: CGFloat(x), y: CGFloat(y))
}
}
}
A couple of suggestions by a design point of view:
You already have all your stars (I guess so) grouped togheter inside a common parent node (that you correctly named Stars). Then you just need to move your node of type Stars and all its child node will move automatically.
Manually changing the coordinates of a node inside an update method does work but (imho) it is not the best way to move it. You should use SKAction instead.
So, if you want to move the stars forever with a common speed and direction you can add this method to Stars
func moveForever() {
let distance = 500 // change this value as you prefer
let seconds : NSTimeInterval = 5 // change this value as you prefer
let direction = CGVector(dx: 0, dy: distance)
let move = SKAction.moveBy(direction, duration: seconds)
let moveForever = SKAction.repeatActionForever(move)
self.runAction(moveForever)
}
Now you can remove the code inside the update method and call moveForever when you want the stars to start moving.
Finally, at some point the stars will leave the screen. I don't know the effect you want to achieve but you will need to deal with this.
for (SKSpriteNode *spriteNode in starSqArray) {
spriteNode.position = CGPoint(x: spriteNode.position.x , y: spriteNode.position.y + GFloat(_starSpeed1))
}
Use the above code in the update() function.
Hope this helps...