I am trying to use a SKView in my login UI for some animations. The SKView is added as a subview to a LoginContainerView as well as the view containing the UITextFields and the sign in button. All of the subviews are positioned with autolayout and the FireScene attached to the SKView has it's scaleMode set to .resizeFill.
The issue I'm having is very clear to observe from this clip: https://streamable.com/5it17 .
Clip explanation: Whenever I double tap any of the UITextFields (to trigger the
UIMenuController / Paste button) the FireScene freezes for a brief
amount of time (yet it's very noticeable) and the FPS drops down to around 30 for 1-2 seconds.
The only node that the FireScene has added as its child node is a modified fireflies SKEmitterNode template. I'm fairly certain that this has nothing to do with the actual code (as it is very straightforward and concise) and is probably a UIKit mixed with SpriteKit bug.
Relevant code:
class FireScene: SKScene {
// MARK: - Properties
var fireSparkEmitter: SKEmitterNode? = nil
// MARK: - Inherited
override func didChangeSize(_ oldSize: CGSize) {
// position the emitter to scene's center
fireSparkEmitter?.position = CGPoint(x: size.width/2, y: size.height/2)
}
override func didMove(to view: SKView) {
if let emitter = fireSparkEmitter {
emitter.resetSimulation()
} else {
guard let emitter = SKEmitterNode(fileNamed: "FireSparks.sks") else { return }
addChild(emitter)
fireSparkEmitter = emitter
}
}
}
class LoginContainerView: UIView {
/// ...
let fireBackgroundScene: FireScene = {
let scene = FireScene(size: .zero)
scene.scaleMode = .resizeFill
return scene
}()
/// ...
// MARK: - Inherited
// called after view initialization
func setupSubviews() {
let skView = SKView(frame: .zero)
skView.translatesAutoresizingMaskIntoConstraints = false
addSubview(skView)
skView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
skView.topAnchor.constraint(equalTo: topAnchor).isActive = true
skView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
skView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
skView.showsFPS = true
skView.presentScene(fireBackgroundScene)
}
}
PS: I've tried to profile the app with Time Profiler and it seems that the bottleneck is the UITextField gesture handling by UIKit itself.
Your issue seems related to a different layout syncronization between SKView and the other UIKit objects.
SKView have a property called isAsynchronous (default = true) that indicates
whether the content is rendered asynchronously.
You can try to set:
skView.isAsynchronous = false
to syncronize your SKView with the core animation updates.
This could be not enough because you'll syncronize SKView with the Core Animations of your project but there are always the UIKit-style animations that could be out of sync.
If this property don't solve your issue you can change also the preferredFramesPerSecond property. Remember that when you try to mix two frameworks (sprite-kit and UIKit) you will inevitably incur to unpleasant hitches.
Related
I'm making a game using spritekit. I'm using a joystick in my game which is declared in a separate SKNode class called 'joystick'.
In this class, I add a UIGesturerecongiser to the view from the class. In the constructor method, one of the parameters is a SKView which is passed from the game scene.
Constructor Method in Joystick class:
init(colour: UIColor, position: CGPoint, skView: SKView) {
//constructor method
self.colour = colour;
self.parentposition = position;
self.view = skView;
super.init();
//setup properties
//user interaction is needed to allow touches to be detected
self.isUserInteractionEnabled = true;
//setup
setup();
}
In the games scene, I initialise the class like this:
class GameScene: SKScene {
func setupJoyStick() {
let joystick1 = Joystick(colour: UIColor.red, position: CGPoint(x: screenwidth / 3, y: screenwidth / 10 * 1.5), skView: self.view!)
self.addChild(joystick1)
}
}
Error:
When I run my app, I get an error because the 'self.view' return nil and because it is forced to unwrap, it causes a fatal error.
Where View is defined:
if let scene = GKScene(fileNamed: "GameScene") {
// Get the SKScene from the loaded GKScene
if let sceneNode = scene.rootNode as! GameScene? {
// Copy gameplay related content over to the scene
sceneNode.entities = scene.entities
sceneNode.graphs = scene.graphs
// Set the scale mode to scale to fit the window
sceneNode.scaleMode = .aspectFill
sceneNode.size = view.bounds.size
// Present the scene
if let view = self.view as! SKView? {
view.presentScene(sceneNode)
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
}
Additional Info:
I'm using:
Xcode 9.2
Swift 4
Sprite Kit
I'm testing it on:
Iphone 6s
Latest version of IOS (non-beta), latest public release.
Can someone please explain why this is happening and how to fix this problem.
Thanks in advance, any help would be appreciated.
Looks like you're trying to get view property of SKScene but it's nil. It's because you didn't presented SKScene.
Unfortunately I haven't worked with SpriteKit but you can find info about view property here and about SKScene here.
I have created a UI elements on main.storyboard which i require to be hidden until the game is over and the once the player tap the screen to dismiss. Main.storyboard is linked to GameViewController therefor all my IBOutlets and IBActions are in there and all my game code is in GameScene. How can i link the view controller to the scene for that the popup image and buttons only appear when it is game over. Would greatly appreciate some help, I have been stuck on this for quite some time now.
This seems to be quite a common problem people have with SpriteKit games so lets go through the difference between SpriteKit games and UIKit apps.
When you make a regular UIKit app, e.g. YouTube, Facebook, you would use ViewControllers, CollectionViews, Views etc for each screen/menu that you see (Home screen, Channel screen, Subscription channel screen etc). So you would use UIKit APIs for this such as UIButtons, UIImageViews, UILabels, UIViews, UICollectionViews etc. To do this visually we would use storyboards.
In SpriteKit games on the other hand it works differently. You work with SKScenes for each screen that you see (MenuScene, SettingsScene, GameScene, GameOverScene etc) and only have 1 ViewController (GameViewController). That GameViewController, which has a SKView in it, will present all your SKScenes.
So we should add our UI directly in the relevant SKScenes using SpriteKit APIs such as SKLabelNodes, SKSpriteNodes, SKNodes etc. To do this visually we would use the SpriteKit scene level editor and not storyboards.
So the general logic would be to load your 1st SKScene as usual from the GameViewController and than do the rest from within the relevant SKScenes. Your GameViewController should basically have next to no code in it beyond the default code. You can also transition from 1 scene to another scene very easily (GameScene -> GameOverScene).
If you use GameViewController for your UI it will get messy really quickly if you have multiple SKScenes because UI will be added to GameViewController and therefore all SKScenes. So you would have to remove/show UI when you transition between scenes and it would be madness.
To add a label in SpriteKit it would be something like this
class GameScene: SKScene {
lazy var scoreLabel: SKLabelNode = {
let label = SKLabelNode(fontNamed: "HelveticaNeue")
label.text = "SomeText"
label.fontSize = 22
label.fontColor = .yellow
label.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
return label
}()
override func didMove(to view: SKView) {
addChild(scoreLabel)
}
}
To make buttons you essentially create a SKSpriteNode and give it a name and then look for it in touchesBegan or touchesEnded and run an SKAction on it for animation and some code after.
enum ButtonName: String {
case play
case share
}
class GameScene: SKScene {
lazy var shareButton: SKSpriteNode = {
let button = SKSpriteNode(imageNamed: "ShareButton")
button.name = ButtonName.share.rawValue
button.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
return button
}()
override func didMove(to view: SKView) {
addChild(shareButton)
}
/// Touches began
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = atPoint(location)
if let nodeName = node.name {
switch nodeName {
case ButtonName.play.rawValue:
// run some SKAction animation and some code
case ButtonName.share.rawValue:
let action1 = SKAction.scale(to: 0.9, duration: 0.2)
let action2 = SKAction.scale(to: 1, duration: 0.2)
let action3 = SKAction.run { [weak self] in
self?.openShareMenu(value: "\(self!.score)", image: nil) // image is nil in this example, if you use a image just create a UIImage and pass it into the method
}
let sequence = SKAction.sequence([action1, action2, action3])
node.run(sequence)
default:
break
}
}
}
}
}
To make this even easier I would create a button helper class, for a simple example have a look at this
https://nathandemick.com/2014/09/buttons-sprite-kit-using-swift/
You can also check out Apple's sample game DemoBots for a more feature rich example.
This way you can have things such as animations etc in the helper class and don't have to repeat code for each button.
For sharing, I would actually use UIActivityController instead of those older Social APIs which might become deprecated soon. This also allows you to share to multiple services via 1 UI and you will also only need 1 share button in your app. It could be a simple function like this in the SKScene you are calling it from.
func openShareMenu(value: String, image: UIImage?) {
guard let view = view else { return }
// Activity items
var activityItems = [AnyObject]()
// Text
let text = "Can you beat my score " + value
activityItems.append(text as AnyObject)
// Add image if valid
if let image = image {
activityItems.append(image)
}
// Activity controller
let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
// iPad settings
if Device.isPad {
activityController.popoverPresentationController?.sourceView = view
activityController.popoverPresentationController?.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
activityController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.init(rawValue: 0)
}
// Excluded activity types
activityController.excludedActivityTypes = [
UIActivityType.airDrop,
UIActivityType.print,
UIActivityType.assignToContact,
UIActivityType.addToReadingList,
]
// Present
view.window?.rootViewController?.present(activityController, animated: true)
}
and then call it like so when the correct button was pressed (see above example)
openShareMenu(value: "\(self.score)", image: SOMEUIIMAGE)
Hope this helps
create reference of GameViewController in GameScene class like this way
class GameScene: SKScene, SKPhysicsContactDelegate {
var referenceOfGameViewController : GameViewController!
}
in GameViewController pass the reference like this way
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = GameScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
scene.referenceOfGameViewController = self
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
}
by using this line you can pass reference to GameScene class
scene.referenceOfGameViewController = self
Now In GameScene class you can access all the variable of GameViewController like this way
referenceOfGameViewController.fbButton.hidden = false
referenceOfGameViewController.gameOverPopUP.hidden = false
I am trying to change the text of a field on a button cick
the button is getting called but the label displayed the old text.
If I use NSUserDefaults to save the value then I will have to close the app and reopen it to see the new value of text field.
Is there any way when a user presses a button the value gets reset instantaneously on the screen?
GameViewController Code
import UIKit
import SpriteKit
class GameViewController: UIViewController {
#IBOutlet var resetText: UIButton!
#IBAction func ResetTextPreseed(sender: AnyObject) {
GameScene().changeText()
}
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene(fileNamed:"GameScene") {
// 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 shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .AllButUpsideDown
} else {
return .All
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
GameScene Code
import SpriteKit
class GameScene: SKScene {
let myLabel = SKLabelNode(fontNamed:"Chalkduster")
var text = "Hello, WOrld"
override func didMoveToView(view: SKView) {
/* Setup your scene here */
myLabel.text = text
myLabel.fontSize = 45
myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
self.addChild(myLabel)
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
func changeText(){
text = "I got changed"
myLabel.text = text
self.addChild(myLabel)
print ("The value of text is \(text)")
}
}
Currently, what you are doing will not work because you are not referencing the current scene but rather making a new instance of a GameScene scene and calling changeText() method on that instance, which has no effect on a current scene.
There is always a debate about should you or shouldn't use UIKit elements with SpriteKit. I would not go into that topic, but in general, SpriteKit and UIKit are different beasts and even if I really like both frameworks I would stick to SpriteKit only as much as I can when it comes to games ... And about some differences... For example, there is a difference between how SKScene renders its nodes vs how views are rendered. Some quotes from docs :
In the traditional view system, the contents of a view are rendered
once and then rendered again only when the model’s contents change.
This model works very well for views, because in practice most view
content is static. SpriteKit, on the other hand, is designed
explicitly for dynamic content. SpriteKit continuously updates the
scene contents and renders it to ensure that animation is smooth and
accurate.
More differences:
Different coordinate systems.
Views are added to the views (not the the scene).
Nodes are added to the scene (not the the view).
More about view's rendering cycle can be found here.
More about how SpriteKit renders a scene can be found here .
So because of these differences you may run (not necessarily of course) into different problems when mixing these two.
You have three solutions (I can think of):
1) Accessing current scene through self.view inside GameViewControllerr Ugly solution IMO, but it will work:
#IBAction func someAction(sender: AnyObject) {
let skView = self.view as! SKView
if let currentScene = skView.scene as? GameScene {
currentScene.changeText()
}
}
2) Nice solution, but again this is just my opinion - Implementing custom buttons using SKSpriteNode. Just search SO about this.
3)Use third party SpriteKit buttons like SKAButton or AGSpriteButton.
I created an .sks particle emitter based on the spark template.
My app is a normal app (not a game). When a user clicks a button, I have a new View controller that shows modally over fullscreen so that I can blur the background.
In this modal, I created a view and gave it a class of SCNView see image below:
How can I load the particle .sks file to do the animation on that viewController on the Particles view?
Update
How to load a SceneKit particle systems in view controller?
As mentioned by #mnuages, you can use .scnp file instead of .sks, which is a SceneKit Particle System.
So the steps are:
Create a SceneKit Particle System, I called it ConfettiSceneKitParticleSystem.scnp
Then in your art-board, select the view and select the class SCNView for it like in the printscreen of the question
In your UIViewController:
class SomeVC: UIViewController {
#IBOutlet weak var particles: SCNView!
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
let particlesNode = SCNNode()
let particleSystem = SCNParticleSystem(named: "ConfettiSceneKitParticleSystem", inDirectory: "")
particlesNode.addParticleSystem(particleSystem!)
scene.rootNode.addChildNode(particlesNode)
particles.scene = scene
}
}
Et Voila...you have you animation :)
.sks files are SpriteKit particle systems. You can also create SceneKit particle systems in Xcode, they are .scnp files.
A .scnp file is basically an archived SCNParticleSystem that you can load with NSKeyedUnarchiver and add to your scene using -addParticleSystem:withTransform:.
It might be easier to create a SpriteKit Particle File (which is what you did). You can add it to your main view in your UIViewController.
Add this somewhere:
extension SKView {
convenience init(withEmitter name: String) {
self.init()
self.frame = UIScreen.main.bounds
backgroundColor = .clear
let scene = SKScene(size: self.frame.size)
scene.backgroundColor = .clear
guard let emitter = SKEmitterNode(fileNamed: name + ".sks") else { return }
emitter.name = name
emitter.position = CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height / 2)
scene.addChild(emitter)
presentScene(scene)
}
}
To use:
override func viewWillAppear(_ animated: Bool) {
view.addSubview(SKView(withEmitter: "ParticleFileName"))
}
I'm making an a game in XCode 7 using Swift 2. I have a variable that I want to pass from the start screen (which is an UIViewController) to the game scene (which is an SKScene). I want the player to select a character in a UIView and play with it in the SKScene. I also want the score that's in the SKScene to show in the game-over screen that's an UIView. I've seen tutorials for passing data between two UIViewControllers and between two SKScenes, but none of them work for this case.
How can I pass a variable from an UIViewController to a SKScene (and vice versa)?
I ran over this same problem yesterday.
Swift 5.2, xcode 12 and targeting iOS 14. Searched high and low. Eventually found the userData attribute of the SKScene.
Note: The app is based on SwiftUI and the SKScene is inside a UIViewRepresentable:
struct SpriteKitContainer: UIViewRepresentable {
typealias UIViewType = SKView
var skScene: SKScene!
#Binding var timerData : ModelProgressTimer
Not that I am passing a binding into the View - this contains a number of data items I wanted to make available to the scene.
I placed code in two places to populate skScene.userData:
func makeUIView(context: Context) -> SKView {
self.skScene.scaleMode = .resizeFill
self.skScene.backgroundColor = .green
debugPrint("setting user data")
self.skScene.userData = [:]
self.skScene.userData!["color"] = UIColor(timerData.flatColorTwo!)
self.skScene.userData!["running"] = timerData.isRunning
self.skScene.userData!["percent"] = timerData.percentComplete
let view = SKView(frame: .zero)
view.preferredFramesPerSecond = 60
// view.showsFPS = true
// view.showsNodeCount = true
view.backgroundColor = .clear
view.allowsTransparency = true
return view
}
func updateUIView(_ view: SKView, context: Context) {
self.skScene.userData = [:]
self.skScene.userData!["running"] = timerData.isRunning
self.skScene.userData!["color"] = UIColor(timerData.flatColorTwo!)
self.skScene.userData!["percent"] = timerData.percentComplete
view.presentScene(context.coordinator.scene)
}
Then, inside the skScene object, I am able to retrieve the userData values:
var item: SKShapeNode! = nil
override func sceneDidLoad() {
let scaleUp = SKAction.scale(by: 2.0, duration: 1.0)
let scaleDown = SKAction.scale(by: 0.5, duration: 1.0)
scaleUp.timingMode = .easeInEaseOut
scaleDown.timingMode = .easeInEaseOut
let sequence = SKAction.sequence([scaleUp,scaleDown])
actionRepeat = SKAction.repeatForever(sequence)
item = SKShapeNode(circleOfRadius: radius)
addChild(item)
}
override func didChangeSize(_ oldSize: CGSize) {
item.fillColor = self.userData!["color"] as! UIColor
item.strokeColor = .clear
let meRunning = self.userData!["running"] as! Bool
if meRunning {
item.run(actionRepeat)
} else {
item.removeAllActions()
}
}
This draws a circle that "pulses" if the "running" boolean is set in the userdata (a Bool value).
If you haven't already found the answer, I did find a way to do this. I was doing something very similar.
In my case I have a UIView that gets called as a pause menu from my scene. I have made the class and variable names a little more ambiguous so they can apply to your situation as well.
class MyView: UIView {
var scene: MyScene!
var button: UIButton!
init(frame: CGRect, scene: MyScene){
super.init(frame: frame)
self.scene = scene
//initialize the button
button.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "buttonTapped"))
}
func buttonTapped(){
self.removeFromSuperview() // this removes my menu from the scene
scene.doTheThing() // this calls the method on my scene
}
Here are the relevant parts of my scene.
class MyScene: SKScene {
func doTheThing(){
//this is a function on the scene. You can pass any variable you want through the function.
}
}
In your situation, it sounds like the first screen is a UIView and the second screen is the SKScene.
You may want to make your SKScene first, pause it, and then add the UIView in front of the Scene. Once the character is selected, you can remove the UIView and add your character into the scene.
I hope that this answers your question. If it doesn't let me know.
From the level select scene:
let scene = GameScene(fileNamed:"GameScene")
scene?.userData = [:]
scene?.userData!["level"] = 21
self.view?.presentScene(scene!)
From within the game scene:
let level = self.userData!["level"] as! Int