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()
}
Related
I am pretty new to SpriteKit so I may be missing something quite obvious.
I am attempting to create an interactive map of the US. I have loaded PNG images for each state and placed a couple of them into the main SKScene using the scene editor.
My goal is wanting to detect when each state is tapped by the user. I initially wrote some code that would find the nodes within a touch location however, this meant that the user could tap the frame and it would be counted as a valid tap. I only want to register touches that happen within the state texture and not the frame. Reading online it was suggested to use SKPhysicsBody to register when a tap takes place. So I changed my code to the following.
class GameScene: SKScene {
override func didMove(to view: SKView) {}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location: CGPoint = self.convertPoint(fromView: touch.location(in: self.view))
let body = self.physicsWorld.body(at: location)
if let state = body?.node, let name = state.name {
state.run(SKAction.run({
var sprite = self.childNode(withName: name) as! SKSpriteNode
sprite.color = UIColor.random()
sprite.colorBlendFactor = 1
}))
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
Now, if I choose the Bounding circle body type everything works as expected (shown above in the screenshot). When I click within the boudning circle it runs the SKAction otherwise it does nothing. However, when I change the body type to Alpha mask (the body type I want) it suddenly stops detecting the state. In fact, it returns the SKPhysicsBody for the MainScene entity.
Any advice on how to fix this would be greatly appreciated.
Thanks.
i can reproduce this behavior (bug?) when using the scene editor. however it goes away if you skip the sks file and initialize your sprites in code. (i acknowledge that setting locations for 50 states is more tedious this way.)
class GameScene: SKScene {
let ok = SKSpriteNode(imageNamed: "ok")
let nm = SKSpriteNode(imageNamed: "nm")
let tx = SKSpriteNode(imageNamed: "tx")
override func didMove(to view: SKView) {
tx.name = "Texas"
nm.name = "New Mexico"
ok.name = "Oklahoma"
//set up physics bodies w/ alpha mask
tx.physicsBody = SKPhysicsBody(texture: tx.texture!, size: tx.texture!.size())
tx.physicsBody?.affectedByGravity = false
nm.physicsBody = SKPhysicsBody(texture: nm.texture!, size: nm.texture!.size())
nm.physicsBody?.affectedByGravity = false
ok.physicsBody = SKPhysicsBody(texture: ok.texture!, size: ok.texture!.size())
ok.physicsBody?.affectedByGravity = false
//then position your sprites and add them as children
}
}
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 wanna implement a game platform and have implemented three swift games(by using single view project) individually. Now I would like to implement a main menu that has three buttons: Game A, Game B and Game C. When users press the button, the correspondent game will start. Could you please tell me how to do that with less efforts? Like Java, we could create a jar for each project and call them later in other java project. Is there similar way in swift? Thanks.
If you have all the code create an unique project. You should create a segue in every buttonTapped method. To do this create a segue in the Storyboard (drag from one VC to another), then click on it and give it an identifier. In the buttonTapped method use performSegueWithIdentifier("your_identifier") and that is it.
Edit:
Ok, i'll try to be more specific to help you. To create the app you want you will need all the code in the same project (all about the three games).
So, in Xcode, you will have to create four viewController (three for the games and another one for the menu). To do this go to your Main.storyboard file (you have it when you create the project by default) and drag the viewController view (right down). Then, you will have to create a custom class for every one of them (inherits from UIViewController). Then in the storyBoard again you choose your custom class (the one you've created) for every viewController.
Talking about the segues, you can learn how to create them here.
It's basically a way to move between your viewControllers. When you create one you have to use performSegueWithIdentifier("yourSegueIdentifier", sender: nil)
Put that code inside the buttonTapped method of your button (learn all about it here).
Well, let me know if you don't understand something. Good luck!
You can create a SpriteKit scene as a main menu for your game, with three different buttons for transition to each game scene. You can create a title using the SKLabelNode and use a Sprite or Shape Node as buttons for your scene.
I experienced adding UIButtons and UILabels to a SpriteKit Scene can get very technical with the problem of positioning them. Due to the fact that the UI Objects are positioned on the view and not on the SpriteKit Scene directly. You can use a SKSpriteNode as a Button and the SKLabelNode as a Title, For a menu scene.
A Sprite kit scene is placed on the UIView and is scaled depending on the scale mode you define. Apples default scale mode .aspectFill requires no adjustment to Sprite kit objects positioning on different Phone device screen sizes.
This is a Custom Class of a SKSpriteNode with the same functionality as a button.
import Foundation
import SpriteKit
class ButtonLabelNode : SKSpriteNode {
let buttonPressed: () -> ()
init(texture: SKTexture?, color: UIColor, size: CGSize, text: String, buttonPressed: #escaping () -> ()) {
self.buttonPressed = buttonPressed
super.init(texture: texture, color: color, size: size)
let label = SKLabelNode(fontNamed: "Futura")
label.fontSize = 50
label.fontColor = SKColor.red
label.position = CGPoint.init(x: 0.0, y: 0.0)
label.zPosition = 1
label.verticalAlignmentMode = .center
label.text = text
self.addChild(label)
self.isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.alpha = 0.8
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.alpha = 1.0
buttonPressed()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
When a touch begins on the SpriteNode the alpha decreases to 0.8 and back to 1.0 as the touch ends, Giving it the same visual affect of a UIButton. In the overridden function 'touchesEnded' there is a function that will be called every time the button is pressed, that function is added in the initializer and can be initialized in your game scene.
override func didMove(to view: SKView) {
let labelNode = LabelNode(texture: nil, color: .white, size: CGSize.init(width: 200, height: 100), text: "Play", buttonPressed: playButton)
labelNode.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
self.addChild(labelNode)
}
func playButton()
{
print("play")
}
If you would like to have buttons of different shapes or a unique shape you can sub class a ShapeNode instead of a SpriteNode and Implement the same Functionality, Here is a example of how to Create a Circle ShapeNode as a button.
import Foundation
import SpriteKit
class SKShapeButton: SKShapeNode {
init(circleOfRadius: CGFloat) {
super.init()
let diameter = circleOfRadius * 2.0
self.path = CGPath(ellipseIn: CGRect(origin: CGPoint.init(x: 0.0 - circleOfRadius, y: 0.0 - circleOfRadius), size: CGSize(width: diameter, height: diameter)), transform: nil)
self.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.alpha = 0.8
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.alpha = 1.0
}
}
Then just make a instance of it in your game scene.
override func didMove(to view: SKView) {
let shapeButton = SKShapeButton(circleOfRadius: 50)
shapeButton.fillColor = .red
shapeButton.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
self.addChild(shapeButton)
}
To transition to each of your game scene's you can implement the code that is in the Game View Controller swift file, That presents the provided game scene.
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = SKScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
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.
I am new to swift and SpriteKit.
I loaded a image and i am trying to position the image on the top, but the image is "cutted" on the top
As you can see on the image, the window maxed, so it dont seems to be scaling problem.
It seems something "eats" the first 40 pixels, i am using the Iphone 4s Emulator.
If i change to the iphone 6 it "eats" more pixels on the top, is there something that "uses the top pixels like the info bar" ?
The code i am using is, something like that, on my code i loop on the images:
options.append(SKSpriteNode(imageNamed:"help"))
//Reload anchor point to top up
options[0].anchorPoint=CGPoint(x:0, y:1)
//try to position the image on the top... BUT IT CUTS MY IMAGE ON THE TOP
options[0].position = CGPoint(x: 0, y: 0)
self.addChild(options[0])
//Set the scene configuration
self.backgroundColor=UIColor(hue: 0.33, saturation: 0.47, brightness: 0.91, alpha: 1)
self.anchorPoint=CGPoint(x: 0, y: 1)
i think you're making things more difficult by changing the anchorpoint of everything around. It makes it harder to conceptualize the coordinate system. You should try to avoid changing the anchor point for the scene itself if you're going to be placing sprites directly on it.
Usually you'll change the anchor point when you want to make the sprite rotate around it's corner instead of it's center. Things like that. If you're just positioning things it makes more sense to keep the default.
This is the easiset way to make sure our scene has the correct coordinates
GameViewController.swift
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
let scene = GameScene(size: skView.bounds.size)
scene.scaleMode = .AspectFill
skView.presentScene(scene)
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue)
} else {
return Int(UIInterfaceOrientationMask.All.rawValue)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
GameScene.swift
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
self.backgroundColor = SKColor.clearColor()
let sprite = SKSpriteNode(color: SKColor.redColor(), size: CGSize(width: 20, height: 20))
// position sprite on top left corner of screen
sprite.position = CGPoint(x: sprite.size.width/2, y: self.size.height - sprite.size.height/2)
self.addChild(sprite)
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
try using this as a starting point. By default spritekit is setup to use an sks file (level editor file) as a template for your scene. I don't think you're using that. With this code we make sure our scene is initialized with correct dimensions when its created.