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