SpriteKit

RSS for tag

Drawing shapes, particles, text, images, and video in two dimensions using SpriteKit.

SpriteKit Documentation

Post

Replies

Boosts

Views

Activity

SKLabelNode keeps jumping back and forth when displaying different numbers with equal number of digits
I'm trying to display a right-aligned timecode in my game. I had expected that digits would all have the same width, but this doesn't seem to be the case in SpriteKit, even though it seems to be the case in AppKit. In SpriteKit, with the default font there is a noticeable difference in width between the digit 1 and the rest (1 is thinner), so whenever displaying a number with the least significant digit 1 all preceding digits shift slightly to the right. This happens even when setting a NSAttributedString with a font that has a fixedAdvance attribute. class GameScene: SKScene { override func didMove(to view: SKView) { let label = SKLabelNode(text: "") view.scene!.addChild(label) // label.horizontalAlignmentMode = .left label.horizontalAlignmentMode = .right var i = 11 Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in label.text = "\(i)" // let font = NSFont(descriptor: NSFontDescriptor(fontAttributes: [.name: "HelveticaNeue-UltraLight", .fixedAdvance: 20]), size: 30)! // let paragraphStyle = NSMutableParagraphStyle() // paragraphStyle.alignment = .right // label.attributedText = NSAttributedString(string: "\(i)", attributes: [.font: font, .foregroundColor: SKColor.labelColor, .paragraphStyle: paragraphStyle]) i += 5 } } } With AppKit, when using SpriteKit's default font HelveticaNeue-UltraLight, this issue doesn't exist, regardless whether I set the fixedAdvance font attribute. class ViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() let font = NSFont(descriptor: NSFontDescriptor(fontAttributes: [.name: "HelveticaNeue-UltraLight"]), size: 30)! // let font = NSFont(descriptor: NSFontDescriptor(fontAttributes: [.name: "HelveticaNeue-Light", .fixedAdvance: 20]), size: 30)! let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .right let textField = NSTextField(labelWithString: "") textField.font = font textField.alignment = .right // textField.alignment = .left textField.frame = CGRect(x: 100, y: 100, width: 100, height: 100) view.addSubview(textField) var i = 11 Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in textField.stringValue = "\(i)" // textField.attributedStringValue = NSAttributedString(string: "\(i)", attributes: [.font: font, .paragraphStyle: paragraphStyle]) i += 5 } } } Is there a solution to this problem? I filed FB15553700.
0
0
454
Oct ’24
Does the SpriteView of an SKScene have layers? Unable to get magnifying glass view to work with scene.
I'm trying to make a magnifying glass that shows up when the user presses a button and follows the user's finger as it's dragged across the screen. I came across a UIKit-based solution (https://github.com/niczyja/MagnifyingGlass-Swift), but when implemented in my SKScene, only the crosshairs are shown. Through experimentation I've found that magnifiedView?.layer.render(in: context) in: public override func draw(_ rect: CGRect) { guard let context = UIGraphicsGetCurrentContext() else { return } context.translateBy(x: radius, y: radius) context.scaleBy(x: scale, y: scale) context.translateBy(x: -magnifiedPoint.x, y: -magnifiedPoint.y) removeFromSuperview() magnifiedView?.layer.render(in: context) magnifiedView?.addSubview(self) } can be removed without altering the situation, suggesting that line is not working as it should. But this is where I hit a brick wall. The view below is shown but not offset or magnified, and any attempt to add something to context results in a black magnifying glass. Does anyone know why this is? I don't think it's an issue with the code, so I'm suspecting its something specific to SpriteKit or SKScene, likely related to how CALayers work. Any pointers would be greatly appreciated. . . . Full code below: import UIKit public class MagnifyingGlassView: UIView { public weak var magnifiedView: UIView? = nil { didSet { removeFromSuperview() magnifiedView?.addSubview(self) } } public var magnifiedPoint: CGPoint = .zero { didSet { center = .init(x: magnifiedPoint.x + offset.x, y: magnifiedPoint.y + offset.y) } } public var offset: CGPoint = .zero public var radius: CGFloat = 50 { didSet { frame = .init(origin: frame.origin, size: .init(width: radius * 2, height: radius * 2)) layer.cornerRadius = radius crosshair.path = crosshairPath(for: radius) } } public var scale: CGFloat = 2 public var borderColor: UIColor = .lightGray { didSet { layer.borderColor = borderColor.cgColor } } public var borderWidth: CGFloat = 3 { didSet { layer.borderWidth = borderWidth } } public var showsCrosshair = true { didSet { crosshair.isHidden = !showsCrosshair } } public var crosshairColor: UIColor = .lightGray { didSet { crosshair.strokeColor = crosshairColor.cgColor } } public var crosshairWidth: CGFloat = 5 { didSet { crosshair.lineWidth = crosshairWidth } } private let crosshair: CAShapeLayer = CAShapeLayer() public convenience init(offset: CGPoint = .zero, radius: CGFloat = 50, scale: CGFloat = 2, borderColor: UIColor = .lightGray, borderWidth: CGFloat = 3, showsCrosshair: Bool = true, crosshairColor: UIColor = .lightGray, crosshairWidth: CGFloat = 0.5) { self.init(frame: .zero) layer.masksToBounds = true layer.addSublayer(crosshair) defer { self.offset = offset self.radius = radius self.scale = scale self.borderColor = borderColor self.borderWidth = borderWidth self.showsCrosshair = showsCrosshair self.crosshairColor = crosshairColor self.crosshairWidth = crosshairWidth } } public func magnify(at point: CGPoint) { guard magnifiedView != nil else { return } magnifiedPoint = point layer.setNeedsDisplay() } private func crosshairPath(for radius: CGFloat) -> CGPath { let path = CGMutablePath() path.move(to: .init(x: radius, y: 0)) path.addLine(to: .init(x: radius, y: bounds.height)) path.move(to: .init(x: 0, y: radius)) path.addLine(to: .init(x: bounds.width, y: radius)) return path } public override func draw(_ rect: CGRect) { guard let context = UIGraphicsGetCurrentContext() else { return } context.translateBy(x: radius, y: radius) context.scaleBy(x: scale, y: scale) context.translateBy(x: -magnifiedPoint.x, y: -magnifiedPoint.y) removeFromSuperview() magnifiedView?.layer.render(in: context) //If above disabled, no change //Possible that nothing's being rendered into context //Could it be that SKScene view has no layer? magnifiedView?.addSubview(self) } }
0
0
484
Nov ’24
How do I stop a node as it is moving around a UIBezierPath and then restart the moving from where it stopped?
I have an oval UIBezierPath with a moving SKSpriteNode, I stop its motion and record the stopped position. I then restart this motion and want it to restart where it initially stopped. Works great if motion is not stopped. Movement is great around entire oval Path. Also works great as long as this stop-restart sequence occurs along the top half of the oval UIBezierPath. However, I have problems along the bottom half of this Path -- it stops okay, but the restart position is not where it previously stopped. My method to create this oval UIBezierePath is as follows: func createTrainPath() { trainRect = CGRect(x: tracksPosX - tracksWidth/2, y: tracksPosY - tracksHeight/2, width: tracksWidth, height: tracksHeight) // these methods come from @DonMag trainPoints = generatePoints(inRect: trainRect, withNumberOfPoints: nbrPathPoints) trainPath = generatePathFromPoints(trainPoints!, startingAtIDX: savedTrainIndex) } // createTrainPath My method to stop this motion is as follows: func stopFollowTrainPath() { guard (myTrain != nil) else { return } myTrain.isPaused = true savedTrainPosition = myTrain.position // also from @DonMag savedTrainIndex = closestIndexInPath( trainPath, toPoint: savedTrainPosition) ?? 0 } // stopFollowTrainPath Finally, I call this to re-start this motion: func startFollowTrainPath() { var trainAction = SKAction.follow(trainPath.cgPath, asOffset: false, orientToPath: true, speed: thisSpeed) trainAction = SKAction.repeatForever(trainAction) myTrain.run(trainAction, withKey: runTrainKey) myTrain.isPaused = false } // startFollowTrainPath Again, great if motion is not stopped. Movement is great around entire oval Path. Again, no problem for stopping and then restarting along top half of oval .. the ohoh occurs along bottom half. Is there something I need to do within GameScene's update method that I am missing? For example, do I need to reconstruct my UIBezierPath? every time my node moves between the top half and the bottom half and therein account for the fact that the node is traveling in the opposite direction from the top half?
2
0
644
Nov ’24
SCNNode into SKScene is deformed when hit an object
Into a SKScene, I add a SCNSphere as a child of SKShapeNode, as depicted below. When the sphere hit another node (the fence in the example) the sphere is deformed as it were elastic. I didn't found any information about elastic properties. Someone know a way to avoid the deformation? import SwiftUI import SpriteKit import SceneKit @main struct MyApp: App { var body: some Scene { WindowGroup {SpriteView(scene: GameSceneSK(size: UIScreen.main.bounds.size))} } } class GameSceneSK: SKScene { override func sceneDidLoad() { var fencePoints = [ CGPoint(x: 300, y: 0), CGPoint(x: 300, y: 400), CGPoint(x: 0, y: 400) ] let fence = SKShapeNode(points: &fencePoints, count: fencePoints.count) fence.physicsBody = SKPhysicsBody(edgeChainFrom: fence.path!) addChild(fence) let sphereGeometry = SCNSphere(radius: 20) let sphereNode = SCNNode(geometry: sphereGeometry) let sphereScnScene = SCNScene() sphereScnScene.rootNode.addChildNode(sphereNode) let ball3D = SK3DNode(viewportSize: CGSize(width: 40, height: 40)) ball3D.scnScene = sphereScnScene let ball = SKShapeNode(circleOfRadius: 20) ball.physicsBody = SKPhysicsBody(circleOfRadius: 20) ball.addChild(ball3D) physicsWorld.gravity = CGVector(dx: 0.2, dy: 0.2) addChild(ball) } }
2
0
590
Nov ’24
SKNode.zPosition causes nodes to flicker by reordering them for 1 frame
When running the sample code below, every 3 seconds the middle sprite is replaced by a new one. When this happens, most of the time a flicker is noticeable. When recording the screen and stepping through the recording frame by frame, I noticed that the flicker is caused by a temporary reordering of the nodes’. Below you find two screenshots of two consecutive frames where the reordering is clearly visible. This only happens for a SpriteKit scene used as an overlay for a SceneKit scene. Commenting out buttons.zPosition = 1 or avoiding the fade in/out animations solves the issue. I have created FB15945016. import SceneKit import SpriteKit class GameViewController: NSViewController { let overlay = SKScene() var buttons: SKNode! var previousButton: SKSpriteNode! var nextButton: SKSpriteNode! var pageContainer: SKNode! var pageViews = [SKNode]() var page = 0 override func viewDidLoad() { super.viewDidLoad() let scene = SCNScene(named: "art.scnassets/ship.scn")! let scnView = self.view as! SCNView scnView.scene = scene overlay.anchorPoint = CGPoint(x: 0.5, y: 0.5) scnView.overlaySKScene = overlay buttons = SKNode() buttons.zPosition = 1 overlay.addChild(buttons) previousButton = SKSpriteNode(systemImage: "arrow.uturn.backward.circle") previousButton.position = CGPoint(x: -100, y: 0) buttons.addChild(previousButton) nextButton = SKSpriteNode(systemImage: "arrow.uturn.forward.circle") nextButton.position = CGPoint(x: 100, y: 0) buttons.addChild(nextButton) pageContainer = SKNode() pageViews = [SKSpriteNode(systemImage: "square.and.arrow.up"), SKSpriteNode(systemImage: "eraser")] overlay.addChild(pageContainer) setPage(0) Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { [self] _ in setPage((page + 1) % 2) } } func setPage(_ page: Int) { pageViews[self.page].run(.sequence([ .fadeOut(withDuration: 0.2), .removeFromParent() ]), withKey: "fade") self.page = page let pageView = pageViews[page] pageView.alpha = 0 pageView.run(.fadeIn(withDuration: 0.2), withKey: "fade") pageContainer.addChild(pageView) } override func viewDidLayout() { overlay.size = view.frame.size } } extension SKSpriteNode { public convenience init(systemImage: String) { self.init() let width = 100.0 let image = NSImage(systemSymbolName: systemImage, accessibilityDescription: nil)!.withSymbolConfiguration(.init(hierarchicalColor: NSColor.black))! let scale = NSScreen.main!.backingScaleFactor image.size = CGSize(width: width * scale, height: width / image.size.width * image.size.height * scale) texture = SKTexture(image: image) size = CGSize(width: width, height: width / image.size.width * image.size.height) } }
1
0
488
Nov ’24
SpriteKit: SKTileMap leaks with `SKTexture(rect: CGRect)` usage
Hello reader, I am facing an issue that I am not able to resolve. I have been able to create a demo project that demonstrates the issue, which I hope enables you to have a look as well and hopefully find a way to resolve it. What is the issue: I am using SKTileMapNode in order to draw tile maps. Instead of using the tilesets as you can use from within the Xcode editor, I prefer to do it all programmatically using tilesheets (for a plethora of reasons that I will leave out of this equation). This is the code of the gameScene: import SpriteKit import GameplayKit class GameScene: SKScene { private let tileSize = CGSize(width: 32, height: 32) override func didMove(to view: SKView) { super.didMove(to: view) let tileSet = createTileSet() let tileMap = SKTileMapNode(tileSet: tileSet, columns: 100, rows: 100, tileSize: tileSize) for column in 0..<tileMap.numberOfColumns { for row in 0..<tileMap.numberOfRows { guard let tileGroup = tileSet.tileGroups.randomElement() else { fatalError() } tileMap.setTileGroup(tileGroup, forColumn: column, row: row) } } addChild(tileMap) } private func createTileSet() -> SKTileSet { let tileSheetTexture = SKTexture(imageNamed: "terrain") var tileGroups = [SKTileGroup]() let relativeTileSize = CGSize(width: tileSize.width/tileSheetTexture.size().width, height: tileSize.height/tileSheetTexture.size().height) for idx in 0...2 { for jdx in 0...2 { let tileTexture = SKTexture(rect: .init(x: CGFloat(idx) * relativeTileSize.width, y: CGFloat(jdx) * relativeTileSize.height, width: relativeTileSize.width, height: relativeTileSize.height), in: tileSheetTexture) let tileDefinition = SKTileDefinition(texture: tileTexture, size: tileSize) let tileGroup = SKTileGroup(tileDefinition: tileDefinition) tileGroups.append(tileGroup) } } let tileSet = SKTileSet(tileGroups: tileGroups) return tileSet } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { presentSceneAgain() } func presentSceneAgain() { if let frame = view?.frame { view?.presentScene(GameScene(size: frame.size), transition: .doorsCloseHorizontal(withDuration: 1.0)) } } } This demo project create a tilemapnode of 100 X 100 tiles. Then, it fills these 10.000 tiles with a random tile from the tilesheet named "terrain.png". This tile sheet contains many tiles, but I only take the 9 tiles (3 X 3) from the lower left corner as a random tile option. Thus, the 10.000 tiles get filled with one of these 9 tiles. So it doesnt look pretty or anything, but that isnt the purpose. Now, to create these 9 tile textures, I use the SKTexture(rectIn:) method on the source texture being "terrain.png". I think the code is quite clear in itself, but so far the explanation. When you run it, you should see the map being rendered. When you tap the scene, the scene will present a new instance of the scene. Not more than that. Now, when you do this, have a look at the RAM usage of the app. You will see it steadily increases over time, each time you click the scene and a new scene is presented. I looked deeper into what is happening, and what I see in the memory graph, is that for every present of the scene that is done, there are 3 SKTexture instances being created that are never released. The first time the scene is rendered, there 11 SKTexture instances allocated (I dont know why there are 11 though. I would expect 10: the source texture and the 9 tile textures). But then as mentioned, after a tap and a new present, I get 14 SKTexture, of which 3 are zombies, see image leak_1. Moreover, Xcode reports multiple additional leaks from Jet and Metal allocations, see image leak_all. As far as I know, the code presented is not retaining any references that it should not, and I suspect this leaks are happening somewhere inside SpriteKit. But I am not able to find exactly where, or how to resolve it. I hope someone can help with this issue.
2
1
490
Nov ’24
Why is SKPhysicsBody not picking up alpha?! SpriteKit for a WatchOS game.
So I'm trying to use SpriteKit to make the background of my game. The walls have alpha 1.0, and the safe area alpha 0 and fully transparent. (e.g. a big black square with a smaller transparent square in the middle of it). Yet sprite kit always assume the entire image is either fully opaque or fully transparent. That defies its purpose isn't it? Is there a way to make this work?
2
0
342
Jan ’25
[tvOS] Reacting to button taps
I've just started working on my first SpriteKit game that will eventually run on both tvOS and iOS and am looking at how to build a "button". So far, I've got a custom node that looks like: class MyButton: SKSpriteNode { ... #if os(tvOS) override var canBecomeFocused: Bool { true } override func didUpdateFocus(...) { ... } #endif } The above let me nicely handle focus changes in tvOS and now I'm looking at reacting to selecting the button. Searching around, all the articles/questions/posts are from 2015-2016 - which is a LOOOONG time ago. Most of the guidance appears to be to add a tap gesture recognizer in the owning scene and getting the scene to hand it off to the button. That seems pretty brittle and I'd much prefer if the button itself is responsible for its own tap management. So, I guess my question is whether I should just add a gesture recognizer to my custom button class? Is this inefficient if I end up having 7-8 buttons on the screen and each one has its own gesture recognizer? Somewhat related, all of the 10-year-old advice is that if we add recognizers to scenes, then they need to be removed from the view controller... however, in the modern day world with SwiftUI, my project doesn't even have a view controller (yet, anyway)... what gesture recognizer lifecycle management do I need in a SpriteKit scene that is presented within a SpriteKitView? Or, is there a better way? I was kind of hoping that overriding pressesBegan() (or something similar) in my custom button might have been triggered on tvOS (like touchesBegan() lets me manage touches for the iOS variant of my app) Any pointers or suggestions would be gladly received. Thanks.
2
0
438
Jan ’25
GKObstacleGraph<GKGraphNode2D> copy() not work (Bad ACCESS) (SpriteKit)
Given a graph with added obstacles I want to make a copy of it. When I make the copy: currentGrath added 20 obstacles. var newGrapth = currentGrath.copy() as? GKObstacleGraph newGrapth2.removeObstacles([newGrapth!.obstacles.first!]) This returns a BAD ACCESS. I don't understand what's going on or what the problem is. If I do this same thing with the main network there is no problem: currentGrath.removeObstacles([currentGrath!.obstacles.first!]) Thanks for the help
0
0
183
1w