I am building a SpriteKit game. The game scene is the parent of a transparent layer above it whose role is to display occasional messages to the player. I want this layer to be transparent and inert most of the time, and of course, never receive touches. As such, I have isUserInteractionEnabled set to false. However, when I add this later to the scene, it blocks all touches below it. What gives?
Edit: the parent of the MessageLayer is the game scene, and the game scene also has isUserInteractionEnabled = false, so I do not believe that the MessageLayer is inheriting the behavior.
Here is my code:
import Foundation
import SpriteKit
class MessageLayer: SKSpriteNode {
init() {
let size = CGSize(width: screenWidth, height: screenHeight)
super.init(texture: nil, color: UIColor.blue, size: size)
self.isUserInteractionEnabled = false
self.name = "MessageLayer"
alpha = 0.6
zPosition = +200
}
override init(texture: SKTexture!, color: UIColor, size: CGSize)
{
super.init(texture: texture, color: color, size: size)
}
required init(coder aDecoder: NSCoder) {
super.init(texture: nil, color: UIColor.clear, size: CGSize(width: 100, height: 100))
}
// MARK: - Functions
func alphaUp() {
alpha = 0.6
}
func alphaDown() {
alpha = 0.0
}
}
Just relay the touches to the scene / other nodes you want to do stuff with.
class TransparentNode: SKSpriteNode {
init(color: SKColor = .clear, size: CGSize) {
super.init(texture: nil, color: color, size: size)
isUserInteractionEnabled = true
}
// touches began on ios:
override func mouseDown(with event: NSEvent) {
// relay event to scene:
scene!.mouseDown(with: event)
}
required init?(coder aDecoder: NSCoder) { fatalError() }
}
class GameScene: SKScene {
lazy var transparentNode: SKSpriteNode = MyNode(size: self.size)
// touches began on ios:
override func mouseDown(with event: NSEvent) {
print("ho")
}
override func didMove(to view: SKView) {
addChild(transparentNode)
isUserInteractionEnabled = false
}
}
And here you can see that the invisible node is sending events properly:
Related
Trying to animate a sprite node within SpriteKit using SwiftUI but not having much success.
Created a touchable sprite, that works.
But tried a texture map, does nothing.
Tried manually changing the textures, does nothing.
import SwiftUI
import SpriteKit
class GameScene: SKScene {
private var cat1: Cat!
private var cat2: Cat!
private var catAnimation: SKAction!
func createCats() {
var textures:[SKTexture] = []
for i in 1...4 {
textures.append(SKTexture(imageNamed: "img\(i)"))
}
print("tex \(textures.count)")
catAnimation = SKAction.animate(withNormalTextures: textures, timePerFrame: 1)
cat1 = Cat(texture: textures.first, color: UIColor.red, size: CGSize(width: 48, height: 48))
cat1.position = CGPoint(x: 64, y: 128)
cat1.color = .magenta
cat1.isUserInteractionEnabled = true
cat1.delegate = self
addChild(cat1)
cat2 = Cat(texture: textures.first, color: UIColor.red, size: CGSize(width: 48, height: 48))
cat2.position = CGPoint(x: 128, y: 128)
cat2.color = .green
cat2.isUserInteractionEnabled = true
cat2.delegate = self
addChild(cat2)
}
override func didMove(to view: SKView) {
createCats()
}
}
extension GameScene: CatDelegate {
func catTouched(cat: Cat) {
print("cat of color \(cat.color)")
cat.run(SKAction.repeat(catAnimation, count: 4))
}
}
protocol CatDelegate {
func catTouched(cat: Cat)
}
class Cat: SKSpriteNode {
var delegate: CatDelegate!
var isEnabled = true
var sendTouchesToScene = true
var colour: SKColor = .blue
var textures:[SKTexture] = []
var count = 0
override init(texture: SKTexture!, color: UIColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
for i in 1...4 {
textures.append(SKTexture(imageNamed: "img\(i)"))
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent!) {
guard isEnabled else { return }
//send touch to scene if you need to handle further touches by the scene
if sendTouchesToScene {
super.touchesBegan(touches, with: event)
}
self.colorBlendFactor = 1.0
//handle touches for cat
delegate?.catTouched(cat: self)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
//
}
}
struct ContentView: View {
var scene: SKScene {
let scene = GameScene()
scene.size = CGSize(width: 256, height: 256)
scene.scaleMode = .fill
scene.backgroundColor = .white
return scene
}
var body: some View {
SpriteView(scene: scene)
.frame(width: 256, height: 256)
.border(Color.red)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension Task where Success == Never, Failure == Never {
static func sleep(seconds: Double) async throws {
let duration = UInt64(seconds * 1000_000_000)
try await sleep(nanoseconds: duration)
}
}
The update function in the class below, which is a subclass of SKSpriteNode, is supposed to change the texture of the class. According to this SO answer, updating the texture property is sufficient for changing a SKSpriteNode's texture. However, this code doesn't work.
Any ideas why?
class CharacterNode: SKSpriteNode {
let spriteSize = CGSize(width: 70, height: 100)
var level = 0
init(level: Int) {
self.level = level
super.init(texture: nil, color: UIColor.clear, size: spriteSize)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(level: Int) {
self.level = level
self.texture = textureForLevel(level: level)
}
func textureForLevel(level: Int) -> SKTexture {
return SKTexture(imageNamed: "TestTexture")
}
Your code works well. In fact this SO answer is correct.
About SKSpriteNode subclass, the update method is a new custom function added by you, because the instance method update(_:) is valid only for SKScene class, this just to say that this function is not performed if it is not called explicitly.
To make a test about your class you can change your code as below( I've changed only textureForLevel method only to make this example):
class CharacterNode: SKSpriteNode {
let spriteSize = CGSize(width: 70, height: 100)
var level = 0
init(level: Int) {
self.level = level
super.init(texture: nil, color: UIColor.clear, size: spriteSize)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(level: Int) {
self.level = level
self.texture = textureForLevel(level: level)
}
func textureForLevel(level: Int) -> SKTexture {
if level == 3 {
return SKTexture(imageNamed: "TestTexture3.jpg")
}
return SKTexture(imageNamed: "TestTexture.jpg")
}
}
class GameScene: SKScene {
override func didMove(to view: SKView) {
let characterNode = CharacterNode(level:2)
characterNode.update(level:2)
addChild(characterNode)
characterNode.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
characterNode.run(SKAction.wait(forDuration: 2.0), completion: {
characterNode.update(level:3)
})
}
}
As you can see when you launch GameScene a characterNode sprite will be shown in the center of the screen. After 2 seconds, the texture will change.
I'm struggling to understand how class initialisation works. From looking at solutions to other problems I have this as an example of the classes in my app.
import Foundation
import SpriteKit
class Ground : SKSpriteNode {
override init(texture: SKTexture!, color: SKColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
self.zPosition = -20
self.name = "Ground";
}
func configure (size: CGSize, position: CGPoint) {
self.size = size
self.position = position
// Set up the Physics
self.physicsBody = SKPhysicsBody(rectangleOf: size)
self.physicsBody?.contactTestBitMask = PhysicsCategory.Player
self.physicsBody?.categoryBitMask = PhysicsCategory.Ground
self.physicsBody?.collisionBitMask = PhysicsCategory.All
self.physicsBody?.affectedByGravity = false
self.physicsBody?.allowsRotation = false
self.physicsBody?.isDynamic = false
self.physicsBody?.mass = 1.99999
}
convenience init(color: SKColor, isActive: Bool = false) {
let size = CGSize(width: 10, height: 10);
self.init(texture:nil, color: color, size: size)
}
required init?(coder aDecoder: NSCoder) {
// Decoding length here would be nice...
super.init(coder: aDecoder)
}
}
I've put the 'configure' function as a kludge fix to let me pass the scene to the class so I can set sizes depending on the size of the device screen. Ideally I would like to just pull this information on initialisation but everything I try throws up errors that I don't understand.
Im not sure which way would be correct but I was wondering firstly, how would I pass arguments to the class to start with.. e.g..
let myClass = Ground(scene: self)
or can I somehow pull the scene information from directly within the class? I can pass info into functions/methods as I did with 'configure' but I couldn't get it to work on initialisation which would certainly be cleaner.
How would you guys do it?
Size
You should not programmatically change the size of a sprite depending on the current device, just use the same size and then let SpriteKit resizing it for you.
Loading from .sks
This initializer
init?(coder aDecoder: NSCoder)
is used when the Sprite is loaded from a .sks file. However in Xcode 7 you cannot pass values from the .sks to the sprite to set the attributes. I'm not sure you are using a .sks file so for now I am just throwing a fatal_error here. In Xcode 8 however you will be able to pass values from the .sks file to your own class.
required init?(coder aDecoder: NSCoder) {
fatalError("Not implemented")
}
Scene
You don't need to pass a reference of the current scene to your class. Every SKNode has the scene property, just use it to retrieve the scene where the node lives. Of course keep in mind that if you invoke .scene before the node has been added to a scene it will returns nil.
Code
This is the final code
class Ground : SKSpriteNode {
init(color: SKColor = .clearColor(), isActive:Bool = false) {
let texture = SKTexture(imageNamed: "ground")
super.init(texture: texture, color: color, size: texture.size())
self.name = "Ground"
self.zPosition = -20
let physicsBody = SKPhysicsBody(texture: texture, size: texture.size())
physicsBody.contactTestBitMask = PhysicsCategory.Player
physicsBody.categoryBitMask = PhysicsCategory.Ground
physicsBody.collisionBitMask = PhysicsCategory.All
physicsBody.affectedByGravity = false
physicsBody.allowsRotation = false
physicsBody.dynamic = false
physicsBody.mass = 1.99999
self.physicsBody = physicsBody
}
required init?(coder aDecoder: NSCoder) {
fatalError("Not implemented")
}
}
Unless I'm missing something else you're trying to do, you should be able to move the initialization of the PhysicsBody and the other properties from you configure() method and into your init() for the class. Then you can pre-size the texture you pass in on instantiation to result in the proper size for the device.
Also, the position property of the SKSpriteNode can be set by the instantiating class. This means you can accomplish the sizing based on device and the setting of the position property right after instantiating it in your scene and before adding it as a child of the scene.
ADDED:
class Ground : SKSpriteNode {
override init(texture: SKTexture!, color: SKColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
}
convenience init(texture: String, scene: SKScene, isActive: Bool = false) {
let texture = SKTexture(imageNamed: "\(yourtexturename)")
self.init(texture:teture, color: color, size: scene.size)
self.size = size
self.position = [YOU CAN NOW SET POSITION BASED ON THE SCENE]
self.physicsBody = SKPhysicsBody(rectangleOf: size)
self.physicsBody?.contactTestBitMask = PhysicsCategory.Player
self.physicsBody?.categoryBitMask = PhysicsCategory.Ground
self.physicsBody?.collisionBitMask = PhysicsCategory.All
self.physicsBody?.affectedByGravity = false
self.physicsBody?.allowsRotation = false
self.physicsBody?.isDynamic = false
self.physicsBody?.mass = 1.99999
self.zPosition = -20
self.name = "Ground";
}
required init?(coder aDecoder: NSCoder) {
// Decoding length here would be nice...
super.init(coder: aDecoder)
}
}
You might not need to override the original init() method but I didn't test anything.
All the tutorials I have followed, make sprites with all their functions and everything in the same class as everything else - GameScene class. Now I am trying to divide the code from the GameScene to different classes but am having troubles with initializing the first class. Because I have almost no code, I will copy and paste all of it.
Error message:
/Users/Larisa/Desktop/Xcode/Igranje/Poskus2/Poskus2/PlayerUros.swift:17:15: Cannot invoke 'SKSpriteNode.init' with an argument list of type '(texture: SKTexture, position: CGPoint, size: () -> CGSize)'
GameScene class:
import SpriteKit
class GameScene: SKScene {
var player: PlayerUros!
override func didMoveToView(view: SKView) {
let pos = CGPointMake(size.width*0.1, size.height/2)
player = PlayerUros(pos: pos)
addChild(player)
player.rotate()
setupBackground()
}
func setupBackground() {
let BImage = SKSpriteNode(imageNamed: "bg-sky")
BImage.anchorPoint = CGPointMake(0, 0)
BImage.position = CGPointMake(0, 0)
BImage.size = CGSizeMake(self.size.width, self.size.height)
addChild(BImage)
}
}
Player class:
import Darwin
import SpriteKit
class PlayerUros: SKSpriteNode {
init(pos: CGPoint) {
let texture = SKTexture(imageNamed: "hero")
super.init(texture: texture, position: pos, size: SKTexture.size(texture))
}
func rotate() {
let rotate = SKAction.rotateByAngle(CGFloat(M_PI), duration: NSTimeInterval(1.5))
let repeatAction = SKAction.repeatActionForever(rotate)
self.runAction(repeatAction)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and GameViewController class (not sure if it matters):
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = GameScene(size: view.bounds.size)
let skView = view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
skView.ignoresSiblingOrder = true
scene.scaleMode = .ResizeFill
skView.presentScene(scene)
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
Try using the line
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
instead of
super.init(texture: texture, position: pos, size: SKTexture.size(texture))
in your Player class. It works for me. Does it work for you?
This is about drawing a TileMap in SpriteKit. I get the following error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attemped to add a SKNode which already has a parent: name:'(null)' texture:['nil'] position:{64, 0} size:{0, 0} rotation:0.00'
This is my GameViewController.swift:
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = GameScene(size: CGSize(width: 1024, height: 768))
// 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 */
scene.scaleMode = .AspectFill
skView.presentScene(scene)
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
This is my GameScene.swift:
import SpriteKit
class GameScene: SKScene {
let dungeon = SKNode()
let readableMap: [[Int]] = [
[01, 00, 01, 01],
[01, 00, 01, 01],
[01, 00, 01, 01],
[00, 01, 00, 01]]
override init(size: CGSize) {
super.init(size: size)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToView(view: SKView) {
/* Setup your scene here */
drawMap()
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(self)
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
func drawMap() {
let map = readableMap.reverse()
let tiles = [[Tile]](count: map.count, repeatedValue: [Tile](count: map[0].count, repeatedValue: Tile()))
for i in 0..<map.count {
for j in 0..<map[0].count {
let tile = tiles[j][i]
tile.position = CGPoint(x: j * Int(tile.tileSize.width), y: i * Int(tile.tileSize.height))
dungeon.addChild(tile)
}
}
}
}
this is the class in Tile.swift:
import SpriteKit
class Tile: SKSpriteNode {
let tileSize: CGSize = CGSize(width: 64, height: 64)
let tileTexture: SKTexture = SKTexture(imageNamed: "o")
}
what could be the reason? Thanks to anyone, I'm still learning.