How can I make my backgrounds use less memory? - ios

I am trying to add a vertical scrolling background to my project. From what I scene on the internet. My background consists of 8 images, each [320x1000px].png files. So what I ended up doing for it was this:
//Layered Nodes
var backgroundNode: SKNode!
override init(size: CGSize) {
super.init(size: size)
scaleFactor = self.size.width / 320.0
// Background
backgroundNode = createBackgroundNode()
addChild(backgroundNode)
}
func createBackgroundNode() -> SKNode {
let backgroundNode = SKNode()
let ySpacing = 1000.0 * scaleFactor
for index in 0...3 {
let node = SKSpriteNode(imageNamed:String(format: "bg%d", index + 1))
node.setScale(scaleFactor)
node.anchorPoint = CGPoint(x: 0.5, y: 0.0)
node.position = CGPoint(x: self.size.width / 2, y: ySpacing * CGFloat(index))
backgroundNode.addChild(node)
}
return backgroundNode
}
Problem is, they use up to 50Mb of the project. I am trying to find a way to do it so it would take much less memory off my game but I can't seem to find it. Is there anything wrong with this? If not, should I best focus on other parts of the project and keep it this way?

You can create background on the go from vector drawing using paint-code app.
Another point. Are your images optimized? If no you can use ImageOptim for free.

Related

SpriteKit scrolling background image showing line between images

I'm learning how to make games in iOS, so I decided to replicate the first level of Mario Bros using my own assets, just to learn how to make them as well.
The issue I'm having right now is that, when creating the scrolling background, using this formula I found on Hacking with Swift, I keep getting a line in between my images.
I've checked that my images have no border in the AI file. (The images are the ones above)
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var player = SKSpriteNode()
private var bg = SKSpriteNode()
override func didMove(to view: SKView) {
let playerTexture = SKTexture(imageNamed: "player")
player = SKSpriteNode(texture: playerTexture)
player.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
addBackground()
self.addChild(player)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
// MARK: UTILITY FUNCTIONS
func addBackground() {
let bgTexture = SKTexture(imageNamed: "bg")
for i in 0 ... 1 {
bg = SKSpriteNode(texture: bgTexture)
//bg.position = CGPoint(x: (i == 0 ? bgTexture.size().width : bgTexture.size().width - 1) * CGFloat(i), y: self.frame.midY)
bg.position = CGPoint(x: (bgTexture.size().width * CGFloat(i)) - CGFloat(1 * i), y: 0)
bg.size.height = self.frame.height
//bg.anchorPoint = CGPoint.zero
bg.zPosition = -10
self.addChild(bg)
let moveLeft = SKAction.moveBy(x: -bgTexture.size().width, y: 0, duration: 5)
let moveReset = SKAction.moveBy(x: bgTexture.size().width, y: 0, duration: 0)
let moveLoop = SKAction.sequence([moveLeft, moveReset])
let moveForever = SKAction.repeatForever(moveLoop)
bg.run(moveForever)
}
}
}
Just create a new project, copy-paste it and you should see it running
I also changed my GameScene.sks size to: 2688 x 1242
And one last change I made to make the background appear in full screen was to set the LaunchScreen as stated in this answer which seems to be an issue in Xcode 12
I understand that the formula from Hacking with Swift post is making the images overlap by 1px, I've tried removing the 1 * i part from it, yet results are not different.
Other thing I did was to verify the images were "joining" perfectly together, and they do, the lines you can see in the image below are from the AI canvas, both images join in between "cactuses".
I also tried running it into a device in case it might be a bug in the simulator:
Your image has a thin black border on the left handside and along the top, 1 pixel wide. It's black. Remove that and try again.

SKSpriteNode init(texture: atlas!.textureNamed("something") loading incorrectly on iPhone X

G'Day,
I have a problem that has appeared in a game I wrote for my kids. After some investigation I have "discovered" the following issue:
In the original game sprite are loaded via
let sprite = SKSpriteNode(texture: atlas!.textureNamed("something")
The texture atlas used has a large number of small textures, "images" in it. This results in a working and happy game on the older devices , iPhone 7, iPad Air, iPad Pro 12.9 inch. However on the iPhone X etc., and on the iPhone X simulator any of the images loaded from the atlas are displayed either as tiny little dots or not at all. There are no compiler errors and no runtime errors. I am assuming this is something to do with screen resolution, but what? If I load the images via an init(imageNamed: "something") call the self same images are correctly scaled and displayed.
I have created a small stand alone program that illustrates this issue, code below: I also included two screen grabs showing the self same code loaded and running on an iPad and an iPhone Xsmax.
The iPad image shows two platforms and the bunnies whilst the iPhone image shows them missing.
The two atlas files contain the platform and GEM and bunny images, the first atlas also contains about 90 small images in total. As you can see the first atlas images are incorrectly loaded, however if I reduce the number of images contained below about 30 images it all works. Oh and the compiler is not complaining or splitting the atlas files up either. So I suppose the final question is in two parts:
1 What is actually happening?
2 How to fix it?
The code is as follows:
//
// GameScene.swift
// SpriteTest
//
//
import SpriteKit
class GameScene: SKScene {
let player = SKSpriteNode(imageNamed: "player")
var atlas :SKTextureAtlas?
var atlas2 :SKTextureAtlas?
var tile : SKNode?
var tile2 : SKNode?
override func didMove(to view: SKView) {
// White background for visibility
backgroundColor = SKColor.white
player.position = CGPoint(x: size.width * 0.1, y: size.height * 0.5)
addChild(player)
// first platform "does not appear on iPhone X"
atlas = SKTextureAtlas(named: "Tiles")
tile = SKSpriteNode(texture: atlas!.textureNamed("platform"))
tile?.zPosition = 50
tile?.physicsBody?.isDynamic = true
tile?.physicsBody?.affectedByGravity = false
tile?.position = CGPoint(x: size.width * 0.1, y : size.height * 0.2)
addChild(tile!)
atlas2 = SKTextureAtlas(named: "Smallatlas")
// Second platform always appears
tile2 = SKSpriteNode(texture: atlas2!.textureNamed("platform"))
tile2?.zPosition = 50
tile2?.physicsBody?.isDynamic = true
tile2?.physicsBody?.affectedByGravity = false
tile2?.position = CGPoint(x: size.width * 0.6, y : size.height * 0.2)
addChild(tile2!)
run(SKAction.repeatForever(
SKAction.sequence([
SKAction.run(addMonster),
SKAction.wait(forDuration: 1.0)
])
))
}
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
func addMonster() {
// First monster "bunny" does not appear on iPhone X
let monster = SKSpriteNode(texture: atlas!.textureNamed("chocolate-bunny150"))
// Determine where to spawn the monster along the Y axis
let actualY = random(min: monster.size.height/2, max: size.height - monster.size.height/2)
// Position the monster slightly off-screen along the right edge,
// and along a random position along the Y axis as calculated above
monster.position = CGPoint(x: size.width + monster.size.width/2, y: actualY)
// Add the monster to the scene
addChild(monster)
// Determine speed of the monster
let actualDuration = random(min: CGFloat(2.0), max: CGFloat(4.0))
// Create the actions
let actionMove = SKAction.move(to: CGPoint(x: -monster.size.width/2, y: actualY),
duration: TimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
monster.run(SKAction.sequence([actionMove, actionMoveDone]))
addMonster2()
}
func addMonster2() {
// Second monster always appears
let monster2 = SKSpriteNode(texture: atlas2!.textureNamed("gem"))
// Determine where to spawn the monster along the Y axis
let actualY = random(min: monster2.size.height/2, max: size.height - monster2.size.height/2)
// Position the monster slightly off-screen along the right edge,
// and along a random position along the Y axis as calculated above
monster2.position = CGPoint(x: size.width + monster2.size.width/2, y: actualY)
// Add the monster to the scene
addChild(monster2)
// Determine speed of the monster
let actualDuration = random(min: CGFloat(2.0), max: CGFloat(4.0))
// Create the actions
let actionMove = SKAction.move(to: CGPoint(x: -monster2.size.width/2, y: actualY),
duration: TimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
monster2.run(SKAction.sequence([actionMove, actionMoveDone]))
}
}
""

Superimpose two textures on an SKSpriteNode

I would like to achieve the effect shown in this gif.
Currently I do this with a series of ~7 png images with a red background and a white line, these are animated through the sprite with an SKAction.
There are a few others additions I would like to make to the sprite, that can change depending on situation, and also I would like to repeat this with a number of colours.
This results in: 6 colours, 7 shine effects, 5 edge effects and 4 corner effects resulting in 136 combinations of textures I would need to create and store.
I feel like there has to be a way to superimpose png's with transparent backgrounds when setting the texture of a sprite but I cannot seem to find a way to do this anywhere.
Is this possible so that I can reduce the number of assets to 22 or do I have to make all 136 and build in logic to the class to decide which to use?
I wanted an effect like this for my game, I tried a lot of options. I tried using particles for performance but couldn't even get close. I know you can accomplish it with Shaders, but i didn't go that route and in iOS 12 Shaders won't be support Open GL anyway. In the end I opted to go with CropNodes.
This is my glare image, it is hard to see because it slightly transparent whiteish image.
This is the results I achieved using CropNodes
class Glare: SKSpriteNode {
var glare = SKSpriteNode()
private var cropNode = SKCropNode()
init(color: UIColor, size: CGSize) {
super.init(texture: nil, color: color, size: size)
alpha = 0.7
zPosition = 10
setupGlare()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupGlare() {
let buffer: CGFloat = 10
let mask = SKSpriteNode(texture: nil, color: .black, size: self.size)
let texture = SKTextureAtlas(named: "Sprites").textureNamed("glare")
glare = SKSpriteNode(texture: texture)
glare.position = CGPoint(x: 0 - (self.size.width / 2 + buffer), y: self.size.height / 2 + buffer)
glare.setScale(3.50)
glare.zPosition = 1
cropNode.zPosition = 2
cropNode.maskNode = mask
cropNode.addChild(glare)
let random = Double(CGFloat.random(min: 0, max: 1))
let pause = SKAction.wait(forDuration: random)
let move = SKAction.moveBy(x: self.size.width + buffer * 2, y: 0 - (self.size.height + buffer * 2), duration: 0.5)
let wait = SKAction.wait(forDuration: 1.0)
let reset = SKAction.moveBy(x: 0 - (self.size.width + buffer * 2), y: self.size.height + buffer * 2, duration: 0.0)
let seq = SKAction.sequence([move, wait, reset])
let repeater = SKAction.repeatForever(seq)
let repeaterSeq = SKAction.sequence([pause, repeater])
glare.run(repeaterSeq)
}
func showGlare(texture: SKTexture) {
let mask = SKSpriteNode(texture: texture)
cropNode.maskNode = mask
glare.isHidden = false
if cropNode.parent == nil { self.addChild(cropNode)}
}
func hideGlare() {
glare.isHidden = true
//remove cropnode from the node tree
cropNode.removeFromParent()
}
}
and then in my GameScene...
I add my glares to a glare layer but that isn't necessary. I also go through when the game loads and create my glares for all 15 slots ahead of time and put them in an array. This way I do not have to create them on the fly and I can just turn on slot 10 any time I want and turn it off as well.
private var glares = [Glare]()
let glare = Glare(color: .clear, size: CGSize(width: kSymbolSize, height: kSymbolSize))
glare.position = CGPoint(x: (CGFloat(x - 1) * kSymbolSize) + (kSymbolSize / 2), y: 0 - (CGFloat(y) * kSymbolSize) + (kSymbolSize / 2))
glare.zPosition = 100
glareLayer.addChild(glare)
glares.append(glare)
When I want to show the glare on a slot
EDIT texture here for you would just be a blank square texture the size of your tile.
glares[index].showGlare(texture: symbol.texture!)
When I want to hide it
glares[index].hideGlare()

SpriteKit joint: follow the body

I've been asked to simplify this question, so that's what I'm doing.
I'm struggling in SpriteKit's physic joints (and possibly physic body properties). I tried every single subclass and many configurations but seams like nothing works or I'm doing something wrong.
I'm developing Snake game. User controls head of snake which should move at constant speed ahead and user can turn it clockwise or anticlockwise. All the remaining snake's pieces should follow the head - they should travel exactly the same path that head was some time ago.
I think for this game the Pin joint should be the answer, which anchor point is exactly in the centre between elements.
Unfortunately the result is not perfect. The structure should make the perfect circle, but it doesn't. I'm attaching the code, and gif showing the current effect. Is anyone experience enough to give me any suggestion what properties of physic body and or joints should are apply here for desired effect?
My code:
class GameScene: SKScene {
private var elements = [SKNode]()
override func didMove(to view: SKView) {
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
let dummyTurnNode = SKNode()
dummyTurnNode.position = CGPoint(x: size.width / 2 - 50, y: size.height / 2)
let dummyTurnBody = SKPhysicsBody(circleOfRadius: 1)
dummyTurnBody.isDynamic = false
dummyTurnNode.physicsBody = dummyTurnBody
addChild(dummyTurnNode)
for index in 0..<5 {
let element = SKShapeNode(circleOfRadius: 10)
let body = SKPhysicsBody(circleOfRadius: 10)
body.linearDamping = 0
// body.mass = 0
element.physicsBody = body
element.position = CGPoint(x: size.width / 2, y: size.height / 2 - 30 * CGFloat(index))
elements.append(element)
addChild(element)
let label = SKLabelNode(text: "A")
label.fontSize = 10
label.fontName = "Helvetica-Bold"
element.addChild(label)
if index == 0 {
element.fillColor = UIColor.blue()
body.velocity = CGVector(dx: 0, dy: 30)
let dummyTurnJoint = SKPhysicsJointPin.joint(withBodyA: dummyTurnBody, bodyB: body, anchor: dummyTurnNode.position)
physicsWorld.add(dummyTurnJoint)
} else {
body.linearDamping = 1
element.fillColor = UIColor.red()
let previousElement = elements[index - 1]
let connectingJoint = SKPhysicsJointPin.joint(withBodyA: previousElement.physicsBody!, bodyB: body, anchor: CGPoint(x: size.width / 2, y: size.height / 2 - 30 * CGFloat(index) + CGFloat(15)))
physicsWorld.add(connectingJoint)
}
}
}
override func update(_ currentTime: TimeInterval) {
let head = elements.first!.physicsBody!
var velocity = head.velocity
velocity.normalize()
velocity.multiply(30)
head.velocity = velocity
}
}
extension CGVector {
var rwLength: CGFloat {
let xSq = pow(dx, 2)
let ySq = pow(dy, 2)
return sqrt(xSq + ySq)
}
mutating func normalize() {
dx /= rwLength
dy /= rwLength
}
mutating func multiply(_ factor: CGFloat) {
dx *= factor
dy *= factor
}
}
"All the remaining snake's pieces should follow the head - they should travel exactly the same path that head was some time ago."
You should note that with Physics joints you are likely going to have variance no matter what you do. Even if you have it close to perfect you'll have rounding errors under the hood making the path not exact.
If all the tail parts are equal you can also use a different approach, this is something I've done for a comet tail. Basically the idea is that you have an array of tail objects and per-frame move move the last tail-object always to the same position as the head-object. If the head-object has a higher z-position the tail is drawn below it.
If you need to keep your tail in order you could vary the approach by storing an array of head-positions (per-frame path) and then place the tail objects along that path in your per-frame update call to the snake.
See my code below for example:
These are you head-object variables:
var tails = [SKEmitterNode]()
var tailIndex = 0
In your head init function instantiate the tail objects:
for _ in 0...MAX_TAIL_INDEX
{
if let remnant = SKEmitterNode(fileNamed: "FireTail.sks")
{
p.tails.append(remnant)
}
}
Call the below per-frame:
func drawTail()
{
if tails.count > tailIndex
{
tails[tailIndex].resetSimulation()
tails[tailIndex].particleSpeed = velocity() / 4
tails[tailIndex].emissionAngle = zRotation - CGFloat(M_PI_2) // opposite direction
tails[tailIndex].position = position
tailIndex = tailIndex < MAX_TAIL_INDEX ? tailIndex + 1 : 0
}
}
The resulting effect is actually really smooth when you call it from the scene update() function.

SpriteKit animation

I need your advice, I'm new in SpriteKit I need to make animation strips. I have 3 solutions to make it, but I need advice that better and less costly for CPU.
1 solution: each stripe - SKSpriteNode with animation and texture
2 solution: background video
3 solution: each stripe - SKShapeNode with animation
This is a simple task , you don't need to build an atlas animation or use SKShapeNode, you can use SKSpriteNode as this code:
var bar = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(40, 200))
barra.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
self.addChild(barra)
Build n bars with random size, and use SKAction to move them.
Whith this approach your animation will be different everytime you launch it.
Code in details:
class GameScene: SKScene {
var bars: [SKSpriteNode]!
var totBars : Int = 50
override func didMoveToView(view: SKView) {
self.backgroundColor = SKColor(red: 131/255, green: 190/255, blue: 177/255, alpha: 1)
let redBarColor = SKColor(red: 204/255, green: 75/255, blue: 75/255, alpha: 1)
let yellowBar = SKColor(red: 253/255, green: 242/255, blue: 160/255, alpha: 1)
// add here your colors
var colorSelected:SKColor = redBarColor
bars = [SKSpriteNode]()
for i in 0..<totBars-1 {
let colorNum = randomNumber(1...2)
switch (colorNum) {
case 1:
colorSelected = redBarColor
case 2:
colorSelected = yellowBar
default:
break
}
let randomWidth = randomCGFloat(5,max:40)
let randomHeight = randomCGFloat(30,max:400)
let bar = SKSpriteNode.init(color: colorSelected, size: CGSizeMake(randomWidth, randomHeight))
bar.zRotation = -45 * CGFloat(M_PI / 180.0)
bar.name = "bar\(i)"
self.addChild(bar)
bars.append(bar)
}
animateBars()
}
func animateBar(bar:SKSpriteNode) {
print("- \(bar.name) start!")
let deltaX = self.randomCGFloat(0,max:self.frame.maxX)
let deltaY:CGFloat = self.frame.maxY/2
let rightPoint = CGPointMake(self.frame.maxX + deltaX,self.frame.maxY + deltaY)
let leftPoint = CGPointMake(-self.frame.maxX + deltaX,-self.frame.maxY + deltaY)
bar.position = rightPoint
let waitBeforeExit = SKAction.waitForDuration(Double(self.randomCGFloat(1.0,max:2.0)))
let speed = self.randomCGFloat(150,max:300)
let move = SKAction.moveTo(leftPoint, duration: self.getDuration(rightPoint, pointB: leftPoint, speed: speed))
bar.runAction(SKAction.sequence([waitBeforeExit,move]), completion: {
print("\(bar.name) reached position")
self.animateBar(bar)
})
}
func animateBars() {
for bar in bars {
animateBar(bar)
}
}
func getDuration(pointA:CGPoint,pointB:CGPoint,speed:CGFloat)->NSTimeInterval {
let xDist = (pointB.x - pointA.x)
let yDist = (pointB.y - pointA.y)
let distance = sqrt((xDist * xDist) + (yDist * yDist));
let duration : NSTimeInterval = NSTimeInterval(distance/speed)
return duration
}
func randomCGFloat(min: CGFloat, max: CGFloat) -> CGFloat {
return CGFloat(Float(arc4random()) / Float(UINT32_MAX)) * (max - min) + min
}
func randomNumber(range: Range<Int> = 1...6) -> Int {
let min = range.startIndex
let max = range.endIndex
return Int(arc4random_uniform(UInt32(max - min))) + min
}
}
If your stripes are simply plain rectangles, you can use SKSpriteNodes and only give them dimensions and a color, then use actions to animate them. you can rotate the rectangles to give the effect you show in the picture.
You could actually build the whole animation in Xcode using the editor, save it in its own SKS file and load the animation using a SKReferenceNode.
To answer your question, Solution 3 is the least costly method for your CPU. Here're several reasons why this is true:
1. The first solution that you're suggesting SKSpriteNode with animation and texture" require the computer to load the texture to the view. If you have multiple .png files, this would mean that the computer would need to load all of these files. But if you were to use SKShapeNode, you would avoid having to do this and you would cheaply create the same looks as the SKShapeNode is not based on an image.
2. Solution 2 is the most costly to your CPU because running a video. This takes a lot of space in your memory which might even create lags if you run it on your phone.
Also, just another thing to note: If you were to extensively use Solution 1 and 2, you would end up using so much memory. But if you use the Solution 3, you will not deal with this.
Another option, in addition to the ones put forth already, would be to write a GLSL shader for the background. If you just want a cool backdrop that doesn't interact with the rest of your game, this would probably be the most performant option, since it would all happen on the GPU.
You could even, conceivably, render the rectangles without requiring any images at all, since they are just areas of color.
There's a decent introduction to the topic here: Battle of Brothers GLSL Introduction.

Resources