0

I'm creating a game for WatchOS 8 using SwiftUI along with SpriteKit which uses the crown for rotating a sprite. I have the crown position tracked in a @State variable via the .digitalCrownRotation() method. The problem is whenever that variable changes, the entire view it's being used in reloads and my game restarts.

My current architecture (with areas omitted to only show logic dealing with the watch crown) for the view and scene is as follows. This architecture restarts the game on every movement of the watch crown.

View:

import SwiftUI
import SpriteKit

let _unboundedScreenSize : CGSize = WKInterfaceDevice.current().screenBounds.size
let _screenSize : CGSize = CGSize(width: _unboundedScreenSize.width - 15, height: _unboundedScreenSize.height - 15)

struct GameView : View {
    @State private var crown : Float = 0.0
    
    var body : some View {
        let scene : GameScene = GameScene(size: _screenSize, crown: crown)
        NavigationView {
            SpriteView(scene : scene)
                .frame(width: scene.size.width, height: scene.size.height)
                .ignoresSafeArea()
                .focusable()
                .digitalCrownRotation($crown, from: -90, through: 90, by: 1, sensitivity: Options.sensitivity, isContinuous: true, isHapticFeedbackEnabled: true)
                .onTapGesture(perform: scene.shoot)
        }
    }
}

Scene:

import Foundation
import SpriteKit
import SwiftUI
import WatchKit

public class GameScene : SKScene {
    /* Other variables */
    private var _crown : Float
    
    required init(size: CGSize, crown: Float) {
        /* Initialize other variables */
        _crown = crown
        super.init(size: size)
        isUserInteractionEnabled = true
    }
    internal func positionGunArm(angle : Float) {
        removeChildren(in: [_gunArm])
        _gunArm.zRotation = CGFloat(angle)
        addChild(_gunArm)
    }
    /* Other game logic unrelated to crown rotation */
    override public func sceneDidLoad() {
        guard !_loaded else { return }
        /* Draw starting sprites */
    }
    
    override public func update(_ interval : TimeInterval) {
        /* Handle other game logic */
        positionGunArm(angle: _crown)
    }
}

I have also tried a similar architecture in the past which moves the state variable into the scene, but it does not update on crown movement.

View:

import SwiftUI
import SpriteKit

let _unboundedScreenSize : CGSize = WKInterfaceDevice.current().screenBounds.size
let _screenSize : CGSize = CGSize(width: _unboundedScreenSize.width - 15, height: _unboundedScreenSize.height - 15)

struct GameView : View {
    let scene : GameScene = GameScene(size: _screenSize)
    var body : some View {
        NavigationView {
            SpriteView(scene : scene)
                .frame(width: scene.size.width, height: scene.size.height)
                .ignoresSafeArea()
                .focusable()
                .digitalCrownRotation(scene.$crown, from: -90, through: 90, by: 1, sensitivity: Options.sensitivity, isContinuous: true, isHapticFeedbackEnabled: true)
                .onTapGesture(perform: scene.shoot)
        }
    }
}

Scene:

import Foundation
import SpriteKit
import SwiftUI
import WatchKit

public class GameScene : SKScene {
    /* Other variables */
    @State internal var crown : Float = 0.0
    
    override required init(size: CGSize) {
        /* Initialize other variables */
        super.init(size: size)
        isUserInteractionEnabled = true
    }
    internal func positionGunArm(angle : Float) {
        removeChildren(in: [_gunArm])
        _gunArm.zRotation = CGFloat(angle)
        addChild(_gunArm)
    }
    /* Other game logic unrelated to crown rotation */
    override public func sceneDidLoad() {
        guard !_loaded else { return }
        /* Draw starting sprites */
    }
    
    override public func update(_ interval : TimeInterval) {
        /* Handle other game logic */
        positionGunArm(angle: crown)
    }
}

Finally, I wanted to try a variation on the first architecture without the scene initialized in the view, but this yields an error due to property initializers running before 'self' is available.

View:

import SwiftUI
import SpriteKit

let _unboundedScreenSize : CGSize = WKInterfaceDevice.current().screenBounds.size
let _screenSize : CGSize = CGSize(width: _unboundedScreenSize.width - 15, height: _unboundedScreenSize.height - 15)

struct GameView : View {
    @State private var crown : Float = 0.0
    let scene : GameScene = GameScene(size: _screenSize, crown: crown)  // <- Throws an error
    var body : some View {
        NavigationView {
            SpriteView(scene : scene)
                .frame(width: scene.size.width, height: scene.size.height)
                .ignoresSafeArea()
                .focusable()
                .digitalCrownRotation($crown, from: -90, through: 90, by: 1, sensitivity: Options.sensitivity, isContinuous: true, isHapticFeedbackEnabled: true)
                .onTapGesture(perform: scene.shoot)
        }
    }
}

Scene: Same as first architecture

Another note, since I need to build for WatchOS 8, I do not have access to the onChange event within the .digitalCrownRotation() function.

Ideally, I would like the crown to be able to handle rotation of an object without it refreshing the view and scene, so that other sprites within the scene can not have their logic reset via the view refresh.

Thank you

dmjman
  • 1
  • 1
  • You are recreating `GameScene` each time since you create it in the `body` of the view (which you should never do in SwiftUI). You should store it with `@State` (or make it an `ObservableObject` and store it with `@StateObject` – jnpdx Apr 26 '23 at 02:20
  • @jnpdx With your help I got it working, big thank you. My biggest flaw was trying to modify the crown variable as a state, instead of modifying the scene itself as an ObservedObject. – dmjman Apr 26 '23 at 23:57

1 Answers1

0

The solution (with guidance from @jnpdx) is to use the scene itself as the binding and not the variable within the scene.

struct GameView : View {
    @ObservedObject private var scene : GameScene = GameScene(size: _screenSize)
    var body : some View {
        NavigationView {
            SpriteView(scene : scene)
                .frame(width: scene.size.width, height: scene.size.height)
                .ignoresSafeArea()
                .focusable()
                .digitalCrownRotation($scene.crown, from: -90, through: 90, by: 1, sensitivity: Options.sensitivity, isContinuous: true, isHapticFeedbackEnabled: true)
                .onTapGesture(perform: scene.shoot)
        }
    }
}
dmjman
  • 1
  • 1