Swift - Sprite Kit floating bubbles stuck to corners - swift

I'm trying to create a floating bubble view on my Watch App. The bubbles can collide & bounce off each other & the sides of the screen. But for some reason the bubbles are appearing out of the view bounds & getting stuck on the sides of the frame instead of bouncing off. This code works as expect on my iOS application but when using the same code in my Watch app, it doesn't.
It doesn't make much sense to me that this exact code works perfectly on my iOS app but not on the Watch App.
I'm passing the below code into a SpriteView in my SwiftUI View
let ballCategory: UInt32 = 0xb0001
let edgeCategory: UInt32 = 0xb0010
var nodeCount = 0
override func sceneDidLoad() {
//set physicsWorld properties
physicsWorld.contactDelegate = self
physicsWorld.gravity = CGVector(dx: 0.0, dy: 0.0)
//set edges as PhysicsBody
let edge = SKPhysicsBody(edgeLoopFrom: self.frame)
edge.friction = 0
edge.categoryBitMask = edgeCategory
self.physicsBody = edge
makebubble()
makebubble()
makebubble()
makebubble()
}
func makebubble() {
let bubbleTexture = SKTexture(imageNamed: "bubble")
let bubble = SKSpriteNode(texture: bubbleTexture)
let bphysicsBody = SKPhysicsBody(circleOfRadius: bubbleTexture.size().height/2)
bphysicsBody.isDynamic = true
bphysicsBody.usesPreciseCollisionDetection = true
bphysicsBody.restitution = 0.5
bphysicsBody.friction = 0
bphysicsBody.angularDamping = 0
bphysicsBody.linearDamping = 0
bphysicsBody.categoryBitMask = ballCategory
bphysicsBody.collisionBitMask = ballCategory | edgeCategory
bphysicsBody.contactTestBitMask = ballCategory | edgeCategory
bubble.physicsBody = bphysicsBody
bubble.name = "bubble"
// Get a random possition within the width of the scene
let x = CGFloat(randomize(number: Int(size.width - 40)))
let y = CGFloat(randomize(number: Int(size.height - 40)))
// position the bubble
bubble.position.x = x
bubble.position.y = y
// Add the bubble
addMyChild(node: bubble)
}
func addMyChild(node:SKSpriteNode){
self.addChild(node)
node.physicsBody!.applyImpulse(CGVector(dx: 10.0, dy: -2.0))
nodeCount += 1
}
// function that returns a random int from 0 to n-1
func randomize(number: Int) -> Int{
return Int(arc4random()) % number
}

This has nothing to to with watchOS and everything to do with the small screen size of Apple Watches. Try running your code on iOS with a frame modifier of width 150 and height 150 and you'll see what I mean; the bubbles will likely stick to the side.
Your bubbles look like they stick to the edges because they slow down over time (due to restitution being 0.5 instead of 1) and it's statistically more probable for a bubble to have its final movement close to the screen edge (since they will eventually move to the edge, bounce off from it, thus slowing down and eventually halting).
Here are 3 things you can do about this:
as mentioned, increase restitution to 1 (this is optional, as it won't solve the "sticking to the edge" problem on its own, but it helps making the slowing down issue better)
detect when the bubbles stop (you can do this by checking the x and y velocity in the update(_:) function of your SKScene) and make a force that moves them slightly in a random direction. If you are in a fancy mood, you can even make a timer to make random, barely noticable forces that act like small air movements (chances are, it will even make the animation a bit more realistic)
create an outside bounding box with slightly non-linear/circular borders and corners to make the bubbles bounce off the walls in a different way

Related

Convert between UIKit and SpriteKit coordinate systems

I'm new to iOS programming, and have almost no experience with SpriteKit, so please forgive me if this is a ridiculous question.
I've been trying to make a basic grid with a 2D array, and I would prefer to work with it from top-left being 0, 0.
After researching the differences in coordinate systems between UIKit and SpriteKit, I came across this answer about Converting Between View and Scene Coordinates but it doesn't seem to change the y value the way I thought it would. I am guessing that I'm not using it right, or maybe this is not what it's meant to do, I don't know.
When I try this:
let convertedCoordinates = convert(cellCoordinates, to: grid)
print(cellCoordinates.y, convertedCoordinates.y)
it doesn't seem to have any effect on the y value.
I have found that when I change to "y: -cy" in the line let cellCoordinates = CGPoint(x: cx, y: cy)
Then it does seem to work the way I am hoping for, but I don't know if that's the only solution or if doing this will work as expected under more complicated situations.
Here is the code I am working with:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
var background: SKShapeNode!
background = SKShapeNode(rectOf: CGSize(width: frame.size.width, height: frame.size.height))
background.fillColor = SKColor.lightGray
self.addChild(background)
let margin = CGFloat(50)
let width = frame.size.width - margin
let height = frame.size.height - margin
let centerX = frame.midX - width / 2
let centerY = frame.midY - height / 2
var grid: SKShapeNode!
grid = SKShapeNode(rectOf: CGSize(width: width, height: height))
grid.strokeColor = SKColor.clear
self.addChild(grid)
let numRows = 2
let numCols = 3
let cellWidth = width / CGFloat(numCols)
for r in 0..<numRows {
for c in 0..<numCols {
let cx = centerX + (cellWidth / 2) + (CGFloat(c) * cellWidth)
let cy = centerY + (cellWidth / 2) + (CGFloat(r) * cellWidth)
//***
let cellCoordinates = CGPoint(x: cx, y: cy)
//***
let cellNode = SKShapeNode(rectOf: CGSize(width: cellWidth, height: cellWidth))
let convertedCoordinates = convert(cellCoordinates, to: grid)
print(cellCoordinates.y, convertedCoordinates.y)
cellNode.strokeColor = SKColor.black
cellNode.lineWidth = 5
cellNode.fillColor = SKColor.darkGray
cellNode.position = convertedCoordinates
let textNode = SKLabelNode(text: String("\(r),\(c)"))
textNode.fontName = "Menlo"
textNode.fontSize = 60
textNode.verticalAlignmentMode = .center
textNode.position = convertedCoordinates
grid.addChild(cellNode)
grid.addChild(textNode)
}
}
}
}
This is more a philosophical answer than an implementation one. As far as somehow flipping SpriteKit's coordinate system, well, you're going to be fighting it constantly. Better to just embrace the system as it is.
The essence of your question though is more one of separation of model and view. When you say
I would prefer to work with it from top-left being 0, 0
what you mean is that mentally you're thinking of the game as a grid of cells with 0,0 at the top left. That's perfectly fine and natural. That's your model of the game. But what are you writing in the code?
let cx = centerX + (cellWidth / 2) + (CGFloat(c) * cellWidth)
let cy = centerY + (cellWidth / 2) + (CGFloat(r) * cellWidth)
let cellCoordinates = CGPoint(x: cx, y: cy)
let convertedCoordinates = convert(cellCoordinates, to: grid)
That's your view struggling to get out. You have the abstract model grid that you're indexing with r,c with 0,0 at the upper left and whose coordinates increase in unit steps down and to the right. Then there's the view of the model, which might depend on screen resolution, aspect ratio, device orientation, whatever. If you keep the two mentally separate, you'll usually find that you can isolate the translation between the two systems to a small interface. In those places you may have to do things like scale the axes or flip one of them, or stretch things in one direction to match aspect ratios.
In a case like this, if you start with your mental model with your preferred 0,0 in the upper left and think about how the game operates, it'll often be in terms of the cells. OK, that suggests that maybe a 2D array or an array of arrays is natural. Maybe the cells will eventually become a class in your game. They'll probably have a node property that stores the SpriteKit node. You might wind up with something like this:
struct boardPosition {
let row: Int
let col: Int
}
class Cell {
let pos: boardPosition
let node: SKNode
init(pos: boardPosition, in board: Board) {
self.pos = pos
node = SKShapeNode(...)
board.node.addChild(node)
}
}
class Board {
let cells: [[Cell]]
let node: SKNode
init(numRows: Int, numColumns: Int) {
...
}
func movePiece(from: boardPosition, to: boardPosition) {
let piece = cell[from.row][from.col].removePiece()
cell[to.row][to.col].addPiece(piece)
}
}
The operation of the game will be in terms of your mental model. The fact that the y-coordinates of the cells' SKNode nodes happen to decrease as the row index increases will be completely buried.
Set all nodes applicable and scene’s anchor point to 0,1 to get it to mount to the top left corner and set your world node’s (if you do not have one, I recommend adding it, it is a basic SKNode that you use to place all of your game nodes in, allowing you to use a separate node for things not applicable to the game world, like hud and overlays) yScale to -1 to have y increment downward instead of upward.
Edit:
When dealing with SKShapeNodes, you do not have to worry about the images being inversed unless you have an obscure shape. When designing the CGPath for the obscure shape, just flip it.
shape.path = shape.path!.copy(using:CGAffineTransform(scaleX:1,y:-1))
The bigger problem is SKShapeNode does not have anchor points. You instead need to move the entire CGPath
To do this, add the following line:
shape.path = shape.path!.copy(using:CGAffineTransform(translationX:shape.frame.width/2,y:shape.frame.height/2))
If dealing with SKSprite nodes in the future....
This will cause your assets to be upside down, so all you would need to do is have your assets flipped before import, use a secondary node to flip the y axis, or assign all nodes with a yScale of -1. Flipping all of your assets prior to import vertically would be the cheapest method, I believe you can flip it inside xcassets as well, but I need to verify that when I get back on a MacOS again.

SKSpriteNode Stuck with init(edgeLoopFrom:) - SKPhysicsBody

I have a separate class called "Floor" with below.
class Floor: SKNode {
override init() {
super.init()
//let edgeFrame = CGRect(origin: CGPoint(x: 1,y: 1), size: CGSize(width: 1078, height: 1950))
//self.physicsBody = SKPhysicsBody(edgeLoopFrom: edgeFrame)
let borderBody = SKPhysicsBody(edgeLoopFrom: self.frame)
borderBody.friction = 0
self.physicsBody = borderBody
// Apply a physics body to the node
// self.physicsBody?.isDynamic = false
// Set the bit mask properties
self.physicsBody?.categoryBitMask = floorCategory
self.physicsBody?.contactTestBitMask = nailDropCategory
self.physicsBody?.collisionBitMask = balloonCategory
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemted")
}
}
I basically have falling SKSpriteNode's that start at the top of the screen and goes to the bottom where it touches the "Floor" and removes itself then restarts from top again. Issue I'm having is all my SKSpriteNode keeps getting stuck on top and not falling through the border around the frame of the screen. How can I tell my application to ignore those specific nodes and let them in? Appreciate any help!
Here is the object that is moving left and right on the screen but it just falls off the side of the screen without the edgeLoop
if let accelerometerData = motionManager.accelerometerData {
if UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft {
balloon.physicsBody?.velocity = CGVector(dx: accelerometerData.acceleration.x * -500.0, dy: 0)
} else {
balloon.physicsBody?.velocity = CGVector(dx: accelerometerData.acceleration.x * 500.0, dy: 0)
}
}
}
What is happening is that your objects are hitting the top boundary of the edge loop and not being able to make it into the scene.
there is several ways you can do you this, if you absolutely needed the edge loop on the sides I would suggest elongating the loop to be higher than the scene and creating the objects inside the loop but above the visible area. However since you haven't given any indication that you actually need the loop on the sides all I would do is get rid of the edge loop detection.
create a box that is the width of the scene and say a 100px high then put a physics body on it of type floorCategory. then put this box 50 px below the bottom of the screen. Assuming that your floor sprite box has an anchorPoint of 0.5, 0.5 this will hide the box below the screen and the top of the box will sit flush with the bottom of the screen.
Now you will be able to detect when your objects hit the bottom of the screen and you will no longer have to worry about them passing through the edge loop at the top.
OR
an example of elongating the loop would be...
You create a rectangle taller than the screen (green border in the image) to apply the edge loop to, not to the screen size itself

Fast-paced SpriteKit game has irregular CPU activity and is jittery/lags despite frame rate staying high - Swift

I'm having the issue on a simple but fast-paced SpriteKit game, but I've reduced my code just to a bouncing ball and still get the issue to a lesser extent:
override func didMove(to view: SKView) {
super.didMove(to: view)
physicsWorld.contactDelegate = self
physicsWorld.speed = 1
physicsWorld.gravity = CGVector(dx: 0.0, dy: 0.0)
let borderBody = SKPhysicsBody(edgeLoopFrom: self.frame)
borderBody.friction = 0
self.physicsBody = borderBody
borderBody.contactTestBitMask = BallCategory
addBall()
}
func addBall() {
let size = CGSize(width: 20, height: 20)
let position = CGPoint(x: frame.width / 2, y: 50)
let texture = SKTexture(image: #imageLiteral(resourceName: "whiteCircle"))
let ball = SKSpriteNode(texture: texture, size: size)
ball.position = position
ball.physicsBody = SKPhysicsBody(circleOfRadius: size.width / 2)
ball.fillColor = .white
ball.lineWidth = 0
addStandardProperties(node: ball, name: "ball", z: 5, contactTest: 0, category: BallCategory)
ball.physicsBody?.isDynamic = true
addChild(ball)
launchBall()
}
func addStandardProperties(node: SKNode, name: String, z: CGFloat, contactTest: UInt32, category: UInt32) {
node.name = name
node.zPosition = z
node.physicsBody?.isDynamic = false
node.physicsBody?.affectedByGravity = false
node.physicsBody?.mass = 0
node.physicsBody?.restitution = 1
node.physicsBody?.friction = 0
node.physicsBody?.linearDamping = 0
node.physicsBody?.angularDamping = 0
node.physicsBody?.angularVelocity = 0
node.physicsBody?.contactTestBitMask = contactTest
node.physicsBody?.categoryBitMask = category
}
func launchBall() {
let ball = childNode(withName: "ball")!
ball.physicsBody?.velocity = CGVector(dx: 0, dy: 500)
}
This code results in a ball (SKSpriteNode) bouncing up and down. When I run this, CPU usage starts at around 10% on my iPhone 6s and then after increases to around 25-30% after maybe 30-60 seconds (no idea why it's increasing). Throughout all of this, the frame rate stays very close to 60 FPS, usually going no lower than 58 FPS (it's the same way when I run the full game).
Almost any time an alert pops up (e.g., text messages, logging into Game Center, etc.), the lag shows up and shows up at random times when I'm running the full game.
I've also tried deleting and re-running the app, cleaning the project, deleting derived data and running in Release mode. None of these worked permanently.
Should I give up on SpriteKit and try another framework? If so, which? Cocos2D?
Any help is appreciated.
This is the result of Apple prioritising system calls over just about everything else.
When the system wants to know something, check something or otherwise do its thing it does so at the mercy of everything else.
No other engine will be able to help with this, there's no way to silence the system's constant activities through code.
You can get a slight improvement by putting on Flight Mode and turning off WIFI and Bluetooth. The system seems to be somewhat aware that it's in a quieter mode and does less because it's got no 4G or other connectivity it can go communicating with.
Further, there's been some pretty big changes to palm rejection in iOS 11 that's played havoc with the first round of iPad Pro models and creative software, creating multi-second rejection of all touch input. When this kind of thing can make it through to a GM you can be pretty sure they're slipping other messiness through.
Here's some complaints about iOS 11 performance: https://www.macrumors.com/2017/09/25/ios-11-app-slowdowns-performance-issues/
Turns out I had 2 SKViews in my view controller. By default, when you start a project as a SpriteKit game, Xcode sets the view controller root/superview of the GameViewController as an SKView. At some point, I had added a second SKView because I didn't intend for the scene to take up the entire screen and I apparently didn't realize that the VC root view was still set as an SKView. So every time GameViewController loaded, it was loading two SKViews, which is why I saw 120 FPS in Xcode.
I fixed the issue by simply removing the SKView class designation from the VC root view.

How do I make 'n' number of spriteNodes appear inside the screen size and move around inside the screen?

I have coded like below:
func spriteCollection(count : Int) -> [SKSpriteNode]{
var spriteArray = [SKSpriteNode]()
for _ in 0..<count {
let sprite = SKSpriteNode(imageNamed: "node.png")
//giving random position to sprites, but problem is some sprites are going invisible. How do I make it appear inside the view?
let x = CGFloat(arc4random_uniform(UInt32(UIScreen.mainScreen().bounds.size.height)))
let y = CGFloat(arc4random_uniform(UInt32(UIScreen.mainScreen().bounds.size.width)))
sprite.position = CGPoint(x: x, y: y)
spriteArray.append(sprite)
}
return spriteArray
}
override func didMoveToView(view: SKView) {
let spriteCollection = spriteCollection(10)
for sprite in spriteCollection{
//facing problem here, not sure how to move it inside the view
let x = CGFloat(arc4random_uniform(UInt32(size.height)))
let y = CGFloat(arc4random_uniform(UInt32(size.width)))
let action = SKAction.moveTo(CGPoint(x: x , y: y), duration: 5)
sprite.runAction(action)
addChild(sprite)
}
}
The problem I'm facing now is, if I pass count 10 to spriteCollection, I can only see 5 or 6 sprites and rest all are hidden. I just want to make all sprites appear inside the screen size and when I perform move action its position should be inside the screen too. I'm working in landscape mode. Could anyone please help me resolve this..
The problem you are running into is the fact that the screen's bounds are oriented to the device when it is in Portrait mode while your view's bounds are based on the screen size when it is in Landscape mode.
One way to solve your issue would be to reverse your x and y calculations when you are working in "screen space"
// Note how width and height appear to be "backward" here. That's because the screen
// coordinates are defined by the portrait orientation of the device,
// but the view will eventually be in a landscape orientation
let x = CGFloat(arc4random_uniform(UInt32(UIScreen.mainScreen().bounds.size.width)))
let y = CGFloat(arc4random_uniform(UInt32(UIScreen.mainScreen().bounds.size.height)))
But I think a better idea would be to wait to move your sprites into their initial positions until after the view has been created and then position them randomly based on the view's bounds and not the screen's bounds

How to generate a new node when another one hits a certain y value

I have a circle moving up a line, and when that circle reaches a certain y point, how can I make it so that another node would generate from below?
Here is the code I currently have for populating the circles, but I am not able to use it with a physics body, as it generates too many nodes and slows down my app:
func createCirclesOnLine(line: CGFloat) {
var currentY : CGFloat = -110
let maxY = self.size.width * 15
let spacing : CGFloat = 120
while currentY < maxY {
let circle = SKSpriteNode(imageNamed: "first#2x")
circle.physicsBody?.dynamic = false
circle.position = CGPointMake(line, currentY)
//circle.physicsBody?.restitution = -900
circle.size = CGSizeMake(75, 75)
// circle.physicsBody = SKPhysicsBody(rectangleOfSize: circle.size)
let up = SKAction.moveByX(0, y: 9000, duration: 90)
circle.runAction(up)
foregroundNode.addChild(circle)
currentY += CGFloat((random() % 400) + 70)
}
Will post more code if necessary.
There are two ways you can go about this. One is to simply check every circle's y position to see if it's above the screen. You'll need a reference to the circles so...
class GameScene: SKScene {
var circles = Array<SKSpriteNode>()
...
In your createCirlcesOnLine function, add each circle to the array as you create it.
...
self.addChild(circle)
circles.append(circle)
Then, in your update method, enumerate through the circles to see if any of them are above the top of the screen.
override func update(currentTime: NSTimeInterval) {
for circle in circles {
if circle.position.y > self.size.height + circle.size.height/2 {
//Send circle back to the bottom using the circle's position property
}
}
}
This solution will work but causes a lot of unnecessary checks on every update cycle.
A second more efficient (and slightly more complicated) recommendation is to add an invisible node above the top of the screen that stretches the screen width. When the circle collides with it, just move it to the bottom of the screen. Look into implementing the SKPhysicsContactDelegate protocol and what needs to happen for that to work. If you run into problems with this solution, post a separate question with those issues.