Why are SKSpriteNodes only partially rendering? - ios

I've coded a basic layout of "cards" for level selection in a game using Swift and SpriteKit. It's basically just 6 level selection cards side by side that have a picture of the level that the user can select. To create them I am running a for-loop and placing the first one in the center of the screen, a padding, then the second one, then padding etc. Each card is an SKSpriteNode from a png image. Each card is approx a 3rd of the device wide and about a 3rd of the devices height.
I create all six of them and then created an action that moves all 6 cards left or right to select which on the player would like. The one in the center is the one that is selected.
Everything works great on iphone simulators (tested on iPhone 6, iPhone 6 plus, iPhone 7 Plus and iPhone 5... all work great). On iPad simulators the first and last card have a portion of the image that doesn't render at all. The first card has about 1/4 of it lost on the left side, the last card has about 1/4 or it lost on the far right side. I tried running it on a physical iPad as well and it has the same issue. When I run it on an iPad Pro 12.7 it gets worse... it cuts off more of the image.
If I choose only to display 5 of the 6 cards they all render great.
If I choose to shrink them down to about 1/4 of the device width and 1/4 of the device height and lessen the padding they render fine.
I tried playing with the Scene and View sizes and scales and didn't have any improvement.
I've tried using different images and there is no changes at all.
I've double checked all zPositions and found no improvement.
I've tried systematically removing all other objects in the scene and still have the problem.
I've put them on their own "layer" which is an SKEffectNode named cardNode. (it's an SKEffectNode because I choose to later blur it when an alert screen comes up in front of it). I thought that putting them onto their own layer might help but it didn't.
I've put physics bodies on the cards just to make sure that they are still "there" and the physics bodies appear in the correct places. If I click on part of the node that isn't rendered it still does behave properly as though it was still rendered in that area.
I can't figure out where to go from here to fix this. Ideally I would like to add more cards yet in the future but getting stuck on this problem.
Here is the code that I have for creating the cards.
let cardNode = SKEffectNode()
let levelCardArray: [String] = [
"BlackBoxLevelCard.png"
,"FruitLevelCard.png"
,"SportsLevelCard.png"
,"BarnLevelCard.png"
,"SeaLevelCard.png"
,"SpaceLevelCard.png"
]
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
let w10 = screenWidth * 0.10
let w40 = screenWidth * 0.40
let w50 = screenWidth * 0.50
let w60 = screenWidth * 0.60
let h50 = screenHeight * 0.50
let cardMargin = w10
let cardSize = CGSize(width: w40, height: w60)
let startPosition = CGPoint(x: w50, y: h50)
override func didMove(to view: SKView) {
let scene = levelSelectionScene(size: view.bounds.size)
scene.backgroundColor = UIColor.black
let skView = view as SKView
skView.ignoresSiblingOrder = true
cardNode.zPosition = 100
self.addChild(cardNode)
for i in 1...levelCardArray.count {
let currentArrayValue = i - 1
let cardSprite = SKSpriteNode(imageNamed: levelCardArray[currentArrayValue])
cardSprite.size = cardSize
cardSprite.position = CGPoint(x: startPosition.x + (CGFloat(currentArrayValue) * (cardSize.width + cardMargin)), y: startPosition.y)
cardSprite.zPosition = cardNode.zPosition
cardSprite.name = "levelCardObject"
cardNode.addChild(cardSprite)
}
Any help or insight would be greatly appreciated. Thanks guys!

I tried all day today to try to find a way to make this work through resizing the scenes and views and haven't had any luck with it at all.
My solution is to check for the device type and if it's an iPad I am reducing the image sizes and buffer between images until it doesn't cut them off. I don't consider this a very good solution, really just a work around until I can find a better way to do it. Thank you guys for your thoughts though. I definitely appreciate it!

Related

UILabel text doesn't appear when using ARKit

I'm programmatically generating a set of UILabels, attaching them to SCNNodes and then placing them in a scene.
The problem is that the text on some of the labels doesn't appear. This occurs (seemingly) randomly.
Here's the code:
var labels = [SCNNode]()
var index: Int
var x: Float
var y: Float
let N = 3
for i in 0 ... N-1 {
for k in 0 ... N-1 {
let node = label()
labels.append(node)
index = labels.count - 1
x = Float(i) * 0.5 - 0.5
y = Float(k) * 0.5 - 0.5
sceneView.scene.rootNode.addChildNode(labels[index])
labels[index].position = SCNVector3Make(x, y, -1)
}
}
and the method to create the label node:
func label() -> SCNNode {
let node = SCNNode()
let label = UILabel(frame: CGRect(x: CGFloat(0), y: CGFloat(0),
width: CGFloat(100), height: CGFloat(50)))
let plane = SCNPlane(width: 0.2, height: 0.1)
label.text = "test"
label.adjustsFontSizeToFitWidth = true
plane.firstMaterial?.diffuse.contents = label
node.geometry = plane
return node
}
The labels themselves always appear correctly, it's just that some of them are blank, with no text.
I've tried playing around with the size of the label, the size of the plane it is attached to, the font size etc - nothing seems to work.
I've also tried enclosing the label creation in DispatchQueue.main.async { ... }, which didn't help either.
I'm moderately new to Swift and iOS, so could easily have missed something very obvious.
Thanks in advance!
EDIT:
(1) Setting label.backgroundColor = UIColor.magenta makes it clear that in fact the label is not being created, but the node / plane is.
Some of the labels are left white (i.e only the SCNNode is being rendered), however after a short delay they sometimes then become magenta and the text will appear. Some of the labels will remain missing though.
(2) It further appears that it's related to the position and orientation of the node (label) relative to the camera. I created a large (10x10) grid of labels, then tested placing the camera at different initial positions in the grid. The likelihood that a node appeared seemed directly related to the distance of the node from the initial camera position. Those nodes directly in front of the camera were always rendered, and those far away almost never were.
(3) workaround / hack is to convert the labels to images, and use them instead - code is at https://github.com/Jordan-Campbell/uiimage-arkit-testing if anyone is interested.
If you are labeling things in AR, 99% of the time it is better to do so in "Screen Space" rather than in "Perspective".
Benefits of labels in Screen Space:
ALWAYS readable, regardless of user's distance from the label
You can use regular UILabels, no need to draw them to an image and then map the image to an SCNPlane.
Your app will have a first party feel to it because Apple uses Screen Space for their labels in all of their AR apps (see Measure).
You will be able to use standard animations on your UILabel, animations are much more complex to set up when working with content in Perspective.
If you are sold on Screen Space, let me know and I'll be happy to put up some code showing you the basics.
Use main thread for creating and adding labels to scene. This will make things faster, and avoid coupling this addition to scene with plane detection this really slows down the rendering. this works at times.
DispatchQueue.main.async {
}
Use a UIView SuperView as Parent to your label this would make things smoother.

Spritekit scaling for universal app

I am having trouble properly setting up my app so that it displays correctly on all devices. I want my game to look best for iPhone and I understand that setting my scene size using GameScene(size: CGSize(width: 1334, height: 750)) and use .aspectFill means that on iPads there will be less space to display things which I'm fine with. The problem is, how do I position my nodes so that they are relative to the each devices frame height and width? I use self.frame.height, self.frame.width, self.frame.midX, etc. for positioning my nodes and when I run my game, it positions things properly considering I run on my iPhone 6, but on my iPad, everything seems blown up and nodes are off the screen. I'm going crazy trying to figure this out
I solved this in my game by using scaleFactors, numbers which tell my app how much to enlarge each length, width, height etc. Then I just make my game look well for one phone, and use that phone's width and height to calculate with which factor I need to enlarge it for other devices. In this example I use the iPhone 4 as a base, but you can use any device just change the numbers according to that device.
Portrait mode:
var widthFactor = UIScreen.main.bounds.width/320.0 //I divide it by the default iPhone 4 width
var heightFactor = UIScreen.main.bounds.height/480.0
Landscape mode:
var widthFactor = UIScreen.main.bounds.width/480.0 //I divide it by the default iPhone 4 landscape width
var heightFactor = UIScreen.main.bounds.height/320.0
Then when you make a node, a coin image for example, multiply its coordinates or width/height by the scaleFactors:
let coin = SKSpriteNode(imageNamed: "coin")
coin.position = CGPoint(x: 25 * widthFactor, y: self.size.height - 70 * heightFactor)
I think what you're looking for might be in this answer: https://stackoverflow.com/a/34878528/6728196
Specifically I think this part is what you're looking for (edited to fit your example):
if UIDevice.current.userInterfaceIdiom == .pad {
// Set things only for iPad
// Example: Adjust y positions using += or -=
buttonNode.position.y += 100
labelNode.position.y -= 100
}
Basically, this just adds or subtracts a certain amount from the iPhone position if the user is using an iPad. It's not too complicated, and you can increase or decrease both x and y values of position by a certain value of percentage of the screen (self.size.width * decimalPercentage).
Another benefit of using this way is that you're just modifying the iPhone positions, so it starts by using the default values that you set. Then if on iPad, it will make changes.
If this is hard to understand let me know so I can clear up the explanation

Getting actual view size in Swift / IOS

Even after reading several posts in frame vs view I still cannot get this working. I open Xcode (7.3), create a new game project for IOS. On the default scene file, right after addChild, I add the following code:
print(self.view!.bounds.width)
print(self.view!.bounds.height)
print(self.frame.size.width)
print(self.frame.size.height)
print(UIScreen.mainScreen().bounds.size.width)
print(UIScreen.mainScreen().bounds.size.height)
I get following results when I run it for iPhone 6s:
667.0
375.0
1024.0
768.0
667.0
375.0
I can guess that first two numbers are Retina Pixel size at x2. I am trying to understand why frame size reports 1024x768 ?
Then I add following code to resize a simple background image to fill the screen:
self.anchorPoint = CGPointMake(0.5,0.5)
let theTexture = SKTexture(imageNamed: "intro_screen_phone")
let theSizeFromBounds = CGSizeMake(self.view!.bounds.width, self.view!.bounds.height)
let theImage = SKSpriteNode(texture: theTexture, color: SKColor.clearColor(), size: theSizeFromBounds)
I get an image smaller than the screen size. Image is displayed even smaller if I choose landscape mode.
I tried multiplying bounds width/height with two, hoping to get actual screen size but then the image gets too big. I also tried frame size which makes the image slightly bigger than the screen.
Main reason for my confusion, besides lack of knowledge is the fact that I've seen this exact example on a lesson working perfectly. Either I am missing something obvious or ?
The frame is given as 1024x768 because it is defined in points, not pixels.
If you want your scene to be the same size as your screen, before the scene is presented in your GameViewController, before:
skView.presentScene(scene)
use this:
scene.size = self.view.frame.size
which will make the scene the exact size of the screen.
Then you could easily make an image fill the scene like so:
func addBackground() {
let bgTexture = SKTexture(imageNamed: "NAME")
let bgSprite = SKSpriteNode(texture: bgTexture, color: SKColor.clearColor(), size: scene.size)
bgSprite.anchorPoint = CGPoint(x: 0, y: 0)
bgSprite.position = self.frame.origin
self.addChild(bgSprite)
}
Also, you may want to read up on the difference between a view's bounds and it's frame.

SpriteKit - Basic Positioning of a label in a background

Very new to Sprite Kit and i'm doing some reading now, but wanted to ask about something that I haven't found the best answer to yet.
I'm doing a tutorial with some code that creates a background, and then adds a label to show a score. I started changing the label code to position it on the top-left corner of the screen.
Here is the code (with my edits to the label, gameLabel):
let screenSize = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
let background = SKSpriteNode(imageNamed: "background")
background.position = CGPoint(x: screenWidth / 10, y: (screenHeight / 15) - 100)
background.blendMode = .Replace
background.zPosition = -1
addChild(background)
gameScore = SKLabelNode(fontNamed: "Chalkduster")
gameScore.text = "Score: 0"
gameScore.position = CGPoint(x: screenWidth / 10, y: (screenHeight / 15) - 100)
gameScore.horizontalAlignmentMode = .Left
gameScore.verticalAlignmentMode = .Top
gameScore.fontSize = 48
addChild(gameScore)
So the label is not displaying in any case, and my assumption is this:
Because i'm adding the background first, the label is getting added within the confines of the background, therefore it needs to be positioned differently, perhaps using the label size instead of the screen size?
My questions:
How can I get the label to always appear in the top left?
The author chose a hard-coded CGPoint for this background image and then said it has to be on an iPad, but i'd like to do it for iPhone 6/plus in landscape as well as iPad. Is there a way I can just make it work on both devices without having to specify a CGPoint like that? Can't it just scale and work regardless?
Thanks and apologies if these are basic questions - i'm going to do my best to continue reading on the subject..
Your question has a simple answer to it. You have forgotten and missed out somethings in your code, and I can show you these things.
In your code you set the background ZPosition to -1, the smallest number will always appear at the back. So if you set the SKLabelNode to a bigger zPosition it will always appear at the front, as maybe there may be a problem with rendering, as I have also experience like these, I fix it this way:
Before you add the LabelNode set it's property to this:
gamescore.zPosition = 0
0, In this case could just be anything bigger than the backgrounds(or the node that you want to appear at the back). So this just tells the compiler that you want the LabelNode to appear at the front, or in front of the Background.
If you want to make a universal app or game, with SpriteKit you will need to add some extra code to your game or app. Since I think that it is better to give you a good tutorial to show you instead of showing you some basics, I will give you a good link on how to do this:
SpriteKit: how to make a universal app
I hope this helps, and don't worry this took me some time to figure out my self.

SKEmitterNode particles clumping together on creation

I have an SKEmitterNode that is acting strange in different versions of iOS. If you look at the screenshot below, you will see that when my scene is loaded, the emitter I am using dumps an odd group of rain drops when the scene first loads.
The strangest part is that this happens in some versions of iOS like 8.1 and 9.2, but in some other versions like 9.0 and 7.0 it runs as expected. (meaning no initial clump of drops, just randomly spaced drops.)
Here is my code in didMoveToView:
let rainEmitterPath:NSString = NSBundle.mainBundle().pathForResource("homeScreenRain", ofType: "sks")!
self.rainEmitter01 = NSKeyedUnarchiver.unarchiveObjectWithFile(rainEmitterPath as String) as! SKEmitterNode
self.rainEmitter01.position = CGPointMake(device.x/2, device.y + 50)
self.rainEmitter01.targetNode = self
self.rainEmitter01.particleZPosition = 6.0
self.rainEmitter01.particleBirthRate = 300
self.rainEmitter01.particleSpeed = 5000 * worldScale
self.rainEmitter01.particleSpeedRange = worldScale * 1000
self.rainEmitter01.particleScale = worldScale
self.rainEmitter01.particleScaleRange = worldScale
self.rainEmitter01.particlePositionRange = CGVector(dx: device.x + 100, dy: 5)
//self.rainEmitter01.advanceSimulationTime(7.0)
self.addChild(rainEmitter01)
I have tried using the advanceSimulationTime method, but for some reason the group of drops still falls even after the delay.
I have also tried varying the y size of the position range because I figured that my height of 5 was causing this, but nothing changed even when I make the height equal to the size of the screen, and a height of 5 still works in other versions of iOS.
Anyone know why this is happening and a solution/workaround?
Thanks in advance.

Resources