I am new to swift and SpriteKit.
I loaded a image and i am trying to position the image on the top, but the image is "cutted" on the top
As you can see on the image, the window maxed, so it dont seems to be scaling problem.
It seems something "eats" the first 40 pixels, i am using the Iphone 4s Emulator.
If i change to the iphone 6 it "eats" more pixels on the top, is there something that "uses the top pixels like the info bar" ?
The code i am using is, something like that, on my code i loop on the images:
options.append(SKSpriteNode(imageNamed:"help"))
//Reload anchor point to top up
options[0].anchorPoint=CGPoint(x:0, y:1)
//try to position the image on the top... BUT IT CUTS MY IMAGE ON THE TOP
options[0].position = CGPoint(x: 0, y: 0)
self.addChild(options[0])
//Set the scene configuration
self.backgroundColor=UIColor(hue: 0.33, saturation: 0.47, brightness: 0.91, alpha: 1)
self.anchorPoint=CGPoint(x: 0, y: 1)
i think you're making things more difficult by changing the anchorpoint of everything around. It makes it harder to conceptualize the coordinate system. You should try to avoid changing the anchor point for the scene itself if you're going to be placing sprites directly on it.
Usually you'll change the anchor point when you want to make the sprite rotate around it's corner instead of it's center. Things like that. If you're just positioning things it makes more sense to keep the default.
This is the easiset way to make sure our scene has the correct coordinates
GameViewController.swift
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// 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 */
let scene = GameScene(size: skView.bounds.size)
scene.scaleMode = .AspectFill
skView.presentScene(scene)
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue)
} else {
return Int(UIInterfaceOrientationMask.All.rawValue)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
GameScene.swift
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
self.backgroundColor = SKColor.clearColor()
let sprite = SKSpriteNode(color: SKColor.redColor(), size: CGSize(width: 20, height: 20))
// position sprite on top left corner of screen
sprite.position = CGPoint(x: sprite.size.width/2, y: self.size.height - sprite.size.height/2)
self.addChild(sprite)
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
try using this as a starting point. By default spritekit is setup to use an sks file (level editor file) as a template for your scene. I don't think you're using that. With this code we make sure our scene is initialized with correct dimensions when its created.
Related
I'm currently implementing a custom camera app, and it turns out that replicating Camera.app's UI is quite tricky!
The question that bothers me the most is of course autorotation. Camera.app creates an illusion that UI doesn't rotate, except for some buttons, but it does rotate! This is clear, because:
Home indicator also rotates with the device
Control Center and Notification Center can be pulled down from the top
According to this Q&A, looks like Camera.app uses viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator), and rotates the elements in opposite direction to create the illusion that nothing actually rotates. But there's a catch: to achieve this effect, views should not use Auto Layout, which makes lots of things more complicated than needed.
Using sample code from the aforementioned Q&A I created a simple app that uses a UIImageView with a screenshot of the Camera.app.
class ViewController: UIViewController {
let imageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(imageView)
imageView.image = UIImage(named: "camera_screenshot.PNG")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
imageView.frame = view.bounds
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.imageView.center = CGPoint(x: self.view.bounds.midX, y: self.view.bounds.midY)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { context in
let deltaTransform = coordinator.targetTransform
let deltaAngle = atan2(deltaTransform.b, deltaTransform.a)
if let currentRotation = self.imageView.layer.value(forKeyPath: "transform.rotation.z") as? CGFloat {
let newRotation = currentRotation + (-1 * deltaAngle + 0.0001)
self.imageView.layer.setValue(newRotation, forKeyPath: "transform.rotation.z")
}
} completion: { context in
// Integralize the transform to undo the extra 0.0001 added to the rotation angle.
var currentTransform = self.imageView.transform
currentTransform.a = round(currentTransform.a)
currentTransform.b = round(currentTransform.b)
currentTransform.c = round(currentTransform.c)
currentTransform.d = round(currentTransform.d)
self.imageView.transform = currentTransform
}
}
override var prefersStatusBarHidden: Bool {
return true
}
}
Good! But not good enough! As you can see in this video the image view doesn't rotate but there's still system animation of rotating the UIWindow (I guess?). Interestingly enough, this system animation appears on iPhone 7 (iOS 13.4.1), but doesn't appear on iPhone 11 Pro (iOS 14.3).
So my questions are:
How to get rid of that "system animation"?
Is it possible to replicate Camera.app UI WITH Auto Layout?
Note: please, do not suggest the approach with multiple UIWindow's because it feels quite hacky to me.
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.
I wanna implement a game platform and have implemented three swift games(by using single view project) individually. Now I would like to implement a main menu that has three buttons: Game A, Game B and Game C. When users press the button, the correspondent game will start. Could you please tell me how to do that with less efforts? Like Java, we could create a jar for each project and call them later in other java project. Is there similar way in swift? Thanks.
If you have all the code create an unique project. You should create a segue in every buttonTapped method. To do this create a segue in the Storyboard (drag from one VC to another), then click on it and give it an identifier. In the buttonTapped method use performSegueWithIdentifier("your_identifier") and that is it.
Edit:
Ok, i'll try to be more specific to help you. To create the app you want you will need all the code in the same project (all about the three games).
So, in Xcode, you will have to create four viewController (three for the games and another one for the menu). To do this go to your Main.storyboard file (you have it when you create the project by default) and drag the viewController view (right down). Then, you will have to create a custom class for every one of them (inherits from UIViewController). Then in the storyBoard again you choose your custom class (the one you've created) for every viewController.
Talking about the segues, you can learn how to create them here.
It's basically a way to move between your viewControllers. When you create one you have to use performSegueWithIdentifier("yourSegueIdentifier", sender: nil)
Put that code inside the buttonTapped method of your button (learn all about it here).
Well, let me know if you don't understand something. Good luck!
You can create a SpriteKit scene as a main menu for your game, with three different buttons for transition to each game scene. You can create a title using the SKLabelNode and use a Sprite or Shape Node as buttons for your scene.
I experienced adding UIButtons and UILabels to a SpriteKit Scene can get very technical with the problem of positioning them. Due to the fact that the UI Objects are positioned on the view and not on the SpriteKit Scene directly. You can use a SKSpriteNode as a Button and the SKLabelNode as a Title, For a menu scene.
A Sprite kit scene is placed on the UIView and is scaled depending on the scale mode you define. Apples default scale mode .aspectFill requires no adjustment to Sprite kit objects positioning on different Phone device screen sizes.
This is a Custom Class of a SKSpriteNode with the same functionality as a button.
import Foundation
import SpriteKit
class ButtonLabelNode : SKSpriteNode {
let buttonPressed: () -> ()
init(texture: SKTexture?, color: UIColor, size: CGSize, text: String, buttonPressed: #escaping () -> ()) {
self.buttonPressed = buttonPressed
super.init(texture: texture, color: color, size: size)
let label = SKLabelNode(fontNamed: "Futura")
label.fontSize = 50
label.fontColor = SKColor.red
label.position = CGPoint.init(x: 0.0, y: 0.0)
label.zPosition = 1
label.verticalAlignmentMode = .center
label.text = text
self.addChild(label)
self.isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.alpha = 0.8
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.alpha = 1.0
buttonPressed()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
When a touch begins on the SpriteNode the alpha decreases to 0.8 and back to 1.0 as the touch ends, Giving it the same visual affect of a UIButton. In the overridden function 'touchesEnded' there is a function that will be called every time the button is pressed, that function is added in the initializer and can be initialized in your game scene.
override func didMove(to view: SKView) {
let labelNode = LabelNode(texture: nil, color: .white, size: CGSize.init(width: 200, height: 100), text: "Play", buttonPressed: playButton)
labelNode.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
self.addChild(labelNode)
}
func playButton()
{
print("play")
}
If you would like to have buttons of different shapes or a unique shape you can sub class a ShapeNode instead of a SpriteNode and Implement the same Functionality, Here is a example of how to Create a Circle ShapeNode as a button.
import Foundation
import SpriteKit
class SKShapeButton: SKShapeNode {
init(circleOfRadius: CGFloat) {
super.init()
let diameter = circleOfRadius * 2.0
self.path = CGPath(ellipseIn: CGRect(origin: CGPoint.init(x: 0.0 - circleOfRadius, y: 0.0 - circleOfRadius), size: CGSize(width: diameter, height: diameter)), transform: nil)
self.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.alpha = 0.8
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.alpha = 1.0
}
}
Then just make a instance of it in your game scene.
override func didMove(to view: SKView) {
let shapeButton = SKShapeButton(circleOfRadius: 50)
shapeButton.fillColor = .red
shapeButton.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
self.addChild(shapeButton)
}
To transition to each of your game scene's you can implement the code that is in the Game View Controller swift file, That presents the provided game scene.
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = SKScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
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 am trying to develop a game which need to have a common background across all the scenes using SpriteKit and Swift. Since the background is common and its actions need to be continuous, I created a custom singleton subclass of SKSpriteNode like this:
class BackgroundNode: SKSpriteNode {
static let sharedBackground = BackgroundNode()
private init()
{
let texture = SKTexture(imageNamed: "Background")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
addActors()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func addActors() {
addClouds()
}
private func addClouds() {
addCloud1()
}
private func addCloud1() {
let cloud1 = SKSpriteNode(imageNamed: "Cloud1")
cloud1.size = CGSizeMake(0.41953125*self.size.width, 0.225*self.size.height)
cloud1.position = CGPointMake(-self.size.width/2, 0.7*self.size.height)
self.addChild(cloud1)
let moveAction = SKAction.moveTo(CGPointMake(self.size.width/2, 0.7*self.size.height), duration: 20)
cloud1.runAction(SKAction.repeatActionForever(moveAction))
}
}
And from the GameScene class I am adding this node to the current view like this:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
BackgroundNode.sharedBackground.size = CGSizeMake(self.size.width, self.size.height)
BackgroundNode.sharedBackground.position = CGPointMake(self.size.width/2, self.size.height/2)
addChild(BackgroundNode.sharedBackground)
}
}
The background image is showing up correctly, but the cloud is not getting added. As of the code above The cloud should appear out of the screen and animate into and then again out of the screen through the other edge, but to verify if it is getting added, I even tried adding the cloud to the center of the screen without any animations. Still the cloud didn't show up. What can be the issue here? And how to fix it?
EDIT
I figured out that the child is actually getting added but is getting added and moving through some points far above the screen. I also figured out that it MIGHT have something to do with anchor point of the cloud, but whatever value I set as anchor point, the cloud always remains on the top right corner of the screen. What can I do about the anchor points to make the clouds appear as it should(considering the lower left corner as (0, 0) is what I want)
Solved the issue. The problem was that I had to manually set the anchor points of the Scene and the node. Setting the anchor point of both the Scene and the node to (0, 0) solved the issue. The new code looks as follows:
GameScene
override func didMoveToView(view: SKView) {
anchorPoint = CGPointMake(0, 0) //Added this
BackgroundNode.sharedBackground.size = CGSizeMake(self.size.width, self.size.height)
BackgroundNode.sharedBackground.position = CGPointMake(0, 0)
addChild(BackgroundNode.sharedBackground)
}
BackgroundNode
private init()
{
let texture = SKTexture(imageNamed: "Background")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
anchorPoint = CGPointMake(0, 0) //Added this
addActors()
}