Animating a sprite with SpriteView under iOS15 - sprite-kit

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)
}
}

Related

CAEmitterLayer Stops Displaying

I adapted this code for SwiftUI to display confetti particles, but sometimes the particle emitter does not work. I've noticed that this often happens after sending to background (not killing the app entirely) and reopening it, or simply letting the app sit for a while then trying again.
I've tried using beginTime as other answers have mentioned (on both the emitter and cells), but that fully breaks things. I've also tried toggling various other emitter properties (birthRate, isHidden). It might have to do with the fact that I'm adapting this with UIViewRepresentable. It seems like the emitter layer just disappears, even though the debug console says its still visible.
class ConfettiParticleView: UIView {
var emitter: CAEmitterLayer!
public var colors: [UIColor]!
public var intensity: Float!
private var active: Bool!
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup() {
colors = [UIColor(Color.red),
UIColor(Color.blue),
UIColor(Color.orange),
]
intensity = 0.7
active = false
emitter = CAEmitterLayer()
emitter.emitterPosition = CGPoint(x: UIScreen.main.bounds.width / 2.0, y: 0) // emit from top of view
emitter.emitterShape = .line
emitter.emitterSize = CGSize(width: UIScreen.main.bounds.width, height: 100) // line spans the whole top of view
// emitter.beginTime = CACurrentMediaTime()
var cells = [CAEmitterCell]()
for color in colors {
cells.append(confettiWithColor(color: color))
}
emitter.emitterCells = cells
emitter.allowsGroupOpacity = false
self.layer.addSublayer(emitter)
}
func startConfetti() {
emitter.lifetime = 1
// i've tried toggling other properties here like birthRate, speed
active = true
}
func stopConfetti() {
emitter.lifetime = 0
active = false
}
func confettiWithColor(color: UIColor) -> CAEmitterCell {
let confetti = CAEmitterCell()
confetti.birthRate = 32.0 * intensity
confetti.lifetime = 15.0 * intensity
confetti.lifetimeRange = 0
confetti.name = "confetti"
confetti.color = color.cgColor
confetti.velocity = CGFloat(450.0 * intensity) // orig 450
confetti.velocityRange = CGFloat(80.0 * intensity)
confetti.emissionLongitude = .pi
confetti.emissionRange = .pi / 4
confetti.spin = CGFloat(3.5 * intensity)
confetti.spinRange = 300 * (.pi / 180.0)
confetti.scaleRange = CGFloat(intensity)
confetti.scaleSpeed = CGFloat(-0.1 * intensity)
confetti.contents = #imageLiteral(resourceName: "confetti").cgImage
confetti.beginTime = CACurrentMediaTime()
return confetti
}
func isActive() -> Bool {
return self.active
}
}
view representable
struct ConfettiView: UIViewRepresentable {
#Binding var isStarted: Bool
func makeUIView(context: Context) -> ConfettiParticleView {
return ConfettiParticleView()
}
func updateUIView(_ uiView: ConfettiParticleView, context: Context) {
if isStarted && !uiView.isActive() {
uiView.startConfetti()
print("confetti started")
} else if !isStarted {
uiView.stopConfetti()
print("confetti stopped")
}
}
}
swiftui view for testing
struct ConfettiViewTest: View {
#State var isStarted = false
var body: some View {
ZStack {
ConfettiView(isStarted: $isStarted)
.ignoresSafeArea()
Button(action: {
isStarted = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
isStarted = false
}
}) {
Text("toggle")
.padding()
.background(Color.white)
}
}
}
}

How to add UIGestureRecongnizer programmatically on custom views that are created via code?

I want to add a UITapGestureRecognizer to my view named SetView. My setviews are created programmatically on another custom view called GridView.
This is what I have tried so far but I am not seeing any action while tapping my subvies.
import UIKit
#IBDesignable
class GridView: UIView {
private(set) lazy var deckOfCards = createDeck()
lazy var grid = Grid(layout: Grid.Layout.fixedCellSize(CGSize(width: 128.0, height: 110.0)), frame: CGRect(origin: CGPoint(x: bounds.minX, y: bounds.minY), size: CGSize(width: bounds.width, height: bounds.height)))
lazy var listOfSetCard = createSetCards()
private func createDeck() -> [SetCard] {
var deck = [SetCard]()
for shape in SetCard.Shape.allShape {
for color in SetCard.Color.allColor {
for content in SetCard.Content.allContent {
for number in SetCard.Number.allNumbers {
deck.append(SetCard(shape: shape, color: color, content: content, rank: number))
}
}
}
}
deck.shuffle()
return deck
}
private func createSetCards() -> [SetView] {
var cards = [SetView]()
for _ in 0..<cardsOnScreen {
let card = SetView()
let contentsToBeDrawn = deckOfCards.removeFirst()
card.combinationOnCard.shape = contentsToBeDrawn.shape
card.combinationOnCard.color = contentsToBeDrawn.color
card.combinationOnCard.content = contentsToBeDrawn.content
card.combinationOnCard.rank = contentsToBeDrawn.rank
/* print(contentsToBeDrawn.color) */
addSubview(card)
cards.append(card)
}
return cards
}
override func layoutSubviews() {
super.layoutSubviews()
for index in listOfSetCard.indices {
let card = listOfSetCard[index]
if let rect = grid[index] {
card.frame = rect.insetBy(dx: 2.5, dy: 2.5)
card.frame.origin = rect.origin
print(card.frame.origin)
}
}
}
Here is the function didTap(sender: UITapGestureRecognizer) that I wrote on SetView:
#objc func didTap(sender: UITapGestureRecognizer) {
switch sender.state {
case .changed,.ended:
let rect = UIBezierPath(rect: bounds)
fillBoundingRect(inRect: rect, color: UIColor.gray)
default:
break
}
And ViewController:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
for _ in 1...12 {
let card = game.drawModelCard()
game.deck.append(card)
}
}
lazy var game = SetGame()
weak var setView : SetView! {
didSet {
let tapGestureRecognizer = UITapGestureRecognizer(target:
setView, action: #selector(SetView.didTap(sender:)))
setView.isUserInteractionEnabled = true
setView.addGestureRecognizer(tapGestureRecognizer)
}
}
}
My subviews(SetViews) should change the background color once tapped.

Second node instance SKPhysicsBody not responding to collision

I've been trying to create a simple game in SK where the ball would bounce off the tops of trampolines that spawn gradually. Collisions work fine with the first trampoline but I'm having problems registering it when ball hits second trampoline spawned in run-time. Anyone might have a clue what I've been doing wrong?
Here is the code.
import SpriteKit
import GameplayKit
class GameScene: SKScene, SKPhysicsContactDelegate {
var ball: SKShapeNode?
var trampoline: SKShapeNode?
var cam: SKCameraNode?
var hasHit: Bool?
var hasCollided: Bool?
var hasMoved: Bool?
func createBall() {
ball = SKShapeNode(circleOfRadius: 50)
ball?.position = CGPoint(x: 0, y: -600)
ball?.fillColor = .red
ball?.strokeColor = .red
let ballPhysics = SKPhysicsBody(circleOfRadius: 50)
ball?.physicsBody = ballPhysics
ballPhysics.categoryBitMask = 1
ballPhysics.collisionBitMask = 0
ballPhysics.contactTestBitMask = 0
ballPhysics.affectedByGravity = false //NO GRAVITY!
ballPhysics.isDynamic = true
addChild(ball!)
}
func createTrampoline(x: CGFloat, y: CGFloat) {
let rect = CGRect(x: x, y: y, width: 100, height: 20)
trampoline = SKShapeNode(rect: rect)
trampoline?.fillColor = .blue
trampoline?.strokeColor = .blue
let trampolinePhysics = SKPhysicsBody(rectangleOf: CGSize(width: 100, height: 20))
trampoline?.physicsBody = trampolinePhysics
trampolinePhysics.categoryBitMask = 2
trampolinePhysics.collisionBitMask = 0
trampolinePhysics.contactTestBitMask = 1
trampolinePhysics.affectedByGravity = false //NO GRAVITY!
trampolinePhysics.isDynamic = true
addChild(trampoline!)
}
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
cam = SKCameraNode()
self.camera = cam
self.backgroundColor = .darkText
hasHit = false
hasCollided = false
hasMoved = false
createBall()
createTrampoline(x: -50, y: -10)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touches: AnyObject in touches {
let location = touches.location(in: self)
let move = SKAction.moveTo(x: location.x, duration: 0.1)
ball?.run(move)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if (!hasMoved!) {
let vect = CGVector(dx: 0, dy: 600)
ball?.physicsBody?.applyImpulse(vect)
ball?.physicsBody?.affectedByGravity = true
hasMoved = true
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if (hasHit == true) {
cam?.position.y = (ball?.position.y)!
}
}
func didBegin(_ contact: SKPhysicsContact) {
let vect = CGVector(dx: 0, dy: 1500)
if (((contact.bodyB.node?.position.y)! - (contact.bodyA.node?.position.y)!) > 0) {
if (!hasCollided!){
hasHit = true
hasCollided = true
print("collision")
contact.bodyB.applyImpulse(vect)
createTrampoline(x: -50, y: ((ball?.position.y)! + CGFloat(3000)))
}
} else {
print("bad coll")
hasCollided = false
}
}
}
Thanks in advance.

SpriteKit isUserInteractionEnabled=false Blocks Touches

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:

How to switch between scenes in Sprite Kit?

What's wrong with my code? Every time I hit the button I'm getting a default grey screen. Button for transition:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if self.nodeAtPoint(location) == self.pause {
var pauseScene = PauseScene(size: self.size)
scene?.paused = true
let skView = self.view as SKView!
skView.ignoresSiblingOrder = true
pauseScene.scaleMode = .AspectFill
pauseScene.size = skView.bounds.size
skView.presentScene(pauseScene)
}
}
}
What I have in that scene that don't want to load:
class PauseScene: SKScene {
let pauseBackground = SKSpriteNode (imageNamed: "pauseBackground")
override func didMoveToView(view: SKView) {
self.pauseBackground.anchorPoint = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
self.pauseBackground.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
self.addChild(pauseBackground)
}
}
Did I miss something?
It looks like your issue is in didMoveToView: in your PauseScene. You don't want to change your anchor point for self.pauseBackground Normally anchor point is based on 0 to 1. (.5,.5) being the center of the sprite. You are setting it to something much higher than that and if you are setting the position to half the width and hight of the scene I don't see why you would want to change the anchor point at all. Looks like just deleting the line of code should fix the issue.
class PauseScene: SKScene {
let pauseBackground = SKSpriteNode (imageNamed: "pauseBackground")
override func didMoveToView(view: SKView) {
self.pauseBackground.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
self.addChild(pauseBackground)
}
}
Also I see you are changing the size of your scene after you init it. I would just init it with the size you want.
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if self.nodeAtPoint(location) == self.pause {
let skView = self.view as SKView!
skView.ignoresSiblingOrder = true
var pauseScene = PauseScene(size: skView.bounds.size)
scene?.paused = true
pauseScene.scaleMode = .AspectFill
skView.presentScene(pauseScene)
}
}
}
Hopefully that helps and makes sense.
Here is an updated answer swift 5, iOS 14 Using swiftUI
import SwiftUI
import SpriteKit
struct ContentView: View {
#State var switcher = false
var scene: SKScene {
let scene = GameScene.shared
scene.size = CGSize(width: 256, height: 256)
scene.scaleMode = .fill
scene.backgroundColor = .red
scene.name = "red"
return scene
}
var scene2: SKScene {
let scene2 = GameScene2.shared
scene2.size = CGSize(width: 256, height: 256)
scene2.scaleMode = .fill
scene2.backgroundColor = .blue
scene2.name = "blue"
return scene2
}
var body: some View {
if switcher {
SpriteView(scene: scene)
.frame(width: 256, height: 256)
.ignoresSafeArea()
.background(Color.red)
.onAppear {
scene2.isPaused = true
}
.onDisappear {
scene2.isPaused = false
}
} else {
SpriteView(scene: scene2)
.frame(width: 256, height: 256)
.ignoresSafeArea()
.background(Color.blue)
.onAppear {
scene.isPaused = true
}
.onDisappear {
scene.isPaused = false
}
}
Button {
withAnimation(.easeInOut(duration: 1.0)) {
switcher.toggle()
}
} label: {
Text("switch")
}
}
}
class GameScene: SKScene {
static var shared = GameScene()
override func update(_ currentTime: TimeInterval) {
// Tells your app to perform any app-specific logic to update your scene.
print("name ",scene!.name)
}
}