SpriteKit calling function from class - swift

This is from a simple game in SpriteKit with a Ball() class that has a function shieldOn() which, for the moment, simply replaces the texture of a single ball to that of a ball surrounded by a shield.
The ball is created like this in GameScene:
func getBall() {
let ball = Ball()
ball.createBall(parentNode: self)
}
Here is the Ball class
class Ball: SKSpriteNode {
func createBall(parentNode: SKNode) {
let ball = SKSpriteNode(texture: SKTexture(imageNamed: "ball2"))
ball.physicsBody = SKPhysicsBody(circleOfRadius: 25)
ball.name = "ball"
parentNode.addChild(ball)
ball.size = CGSize(width: 50, height: 50)
ball.position = CGPoint(x: 20, y: 200)
launch(spriteNode: ball, parentNode: parentNode)
}
private func launch(spriteNode: SKSpriteNode, parentNode: SKNode) {
spriteNode.physicsBody?.applyImpulse(CGVector(dx: 5, dy: 0))
}
func shieldOn() {
self.texture = SKTexture(imageNamed: "ballShield")
}
func shieldOff() {
self.texture = SKTexture(imageNamed: "ball2")
}
}
In the main section of my code (GameScene.swift) I don't have a reference to the ball. So I cycle through all of the nodes on the screen and try to cast the matching one as shown below. I crash with an error saying that it could not cast value of type SKSpriteNode to Ball.
for node in self.children {
if node.name == "ball" {
let ball = node as! Ball
ball.shieldOn()
}
}
I've tried a few variations with no luck. Am I at least working in the right direction? Thanks!

With the new information I think you want something like this:
Ball Class:
class Ball: SKSpriteNode{
init() {
let texture = SKTexture(imageNamed: "ball2")
let size = CGSize(width: 50, height: 50)
super.init(texture: texture, color: UIColor.clear, size: size)
self.name = "ball"
self.physicsBody = SKPhysicsBody(circleOfRadius: size.height/2)
self.physicsBody?.applyImpulse(CGVector(dx: 5, dy: 0))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func shieldOn() {
self.texture = SKTexture(imageNamed: "ballShield")
}
func shieldOff() {
self.texture = SKTexture(imageNamed: "ball2")
}
}
Then use this to create the ball:
func getBall() {
let ball = Ball()
ball.position = CGPoint(x: 20, y: 200)
scene?.addChild(ball)
}

Perhaps a better way to do this would be to keep an array of all Balls created and added to the scene. Then you could just iterate through your array and update their texture. You would not need to enumerate them on the screen, which can decrease performance if there are many moving sprites.
As far as your code goes, it looks like you might be affected by this bug:
https://forums.developer.apple.com/thread/26362

Related

Sprite-Kit animations — using SKTextureFilteringMode.nearest

I'm trying to apply SKTextureFilteringMode.nearest to all the frames in my animation.
Previously, when I was using a non-animated sprite, the following worked:
super.init(texture: texture, color: .clear, size: playerSize)
self.texture?.filteringMode = SKTextureFilteringMode.nearest;
Now I've added animation frames (see full code below), this doesn't work — the sprite is blurry. I can't work out how to add this filtering mode to all frames.
class Player: SKSpriteNode {
private var playerAtlas: SKTextureAtlas {
return SKTextureAtlas(named: "Player")
}
private var playerRunTextures: [SKTexture] {
return [
playerAtlas.textureNamed("run1"),
playerAtlas.textureNamed("run2"),
playerAtlas.textureNamed("run3"),
playerAtlas.textureNamed("run4")
]
}
func startRunAnimation() {
let runAnimation = SKAction.animate(with: playerRunTextures, timePerFrame: 0.1)
self.run(SKAction.repeatForever(runAnimation), withKey: "playerRunAnimation")
}
init() {
let texture = SKTexture(imageNamed: "player")
let playerSize = CGSize(width: 30, height: 50)
super.init(texture: texture, color: .clear, size: playerSize)
self.texture?.filteringMode = SKTextureFilteringMode.nearest;
self.position = CGPoint(x: 100, y: 400)
self.startRunAnimation()
}
...
set SKTextureFilteringMode for each texture when you create the sprite animation array:
private var playerRunTextures: [SKTexture] {
let spritesheet = [
"run1",
"run2",
"run3",
"run4"
]
return spritesheet.map { name in
let t1 = playerAtlas.textureNamed(name)
t1.filteringMode = .nearest //<--- for each texture in the atlas
return t1
}
}

spawning random enemies with an array

I am currently making a game where I need random enemies from my array, to spawn in a random location on repeat. This code seems to work okay other than the fact that it can only rotate through each Enemy once. It comes up with an error saying "Attemped to add a SKNode which already has a parent". Any help? Here is my current code:
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
func spawnEnemy() {
let EnemyArray = [Enemy1, Enemy2, Enemy3, Enemy4, Enemy5, Enemy6]
let randomElement = EnemyArray.randomElement()!
self.addChild(randomElement)
var moveEnemy = SKAction.moveTo(y: -800, duration: 4.0)
let deleteEnemy = SKAction.removeFromParent()
let EnemySequence = SKAction.sequence([moveEnemy, deleteEnemy])
randomElement.run(EnemySequence)
}
func runEnemy() {
run(SKAction.repeatForever(SKAction.sequence([SKAction.run(spawnEnemy), SKAction.wait(forDuration: 2.0)])))
}
as jnpdx suggested, you should spawn new instances of your Enemy class rather than starting with an array of them. you can introduce randomness inside the Enemy class -- for example a random start position or a random color. i would also put your movement and removeFromParent code inside the class as well. You didn't post your Enemy code, but it might look something like this
class Enemy:SKNode {
var shape:SKShapeNode?
override init() {
super.init()
//how ever you want to graphically represent your enemy... using a SKShapeNode for demo
shape = SKShapeNode(ellipseOf: CGSize(width: 20, height: 40))
shape?.fillColor = .blue
addChild(shape ?? SKNode())
//randomize starting x position
position.x = CGFloat.random(in: -200...200)
position.y = 200
move()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//move and remove this node using SKAction
func move() {
let move = SKAction.moveTo(y: -200, duration: 4.0)
let delete = SKAction.removeFromParent()
let sequence = SKAction.sequence([move, delete])
self.run(sequence)
}
}
then you would simply activate your spawn point from didMove(to view: SKView) like this
override func didMove(to view: SKView) {
runSpawnPoint()
}
func runSpawnPoint() {
run(SKAction.repeatForever(SKAction.sequence([SKAction.run(spawnEnemy), SKAction.wait(forDuration: 2.0)])))
}
func spawnEnemy() {
let enemy = Enemy() //a brand new Enemy object each time
addChild(enemy)
}
optional: save your spawned Enemy objects in an array if you want to access them later. alternately you can simply query self.children from your SKScene since they're all stored there as well. in which case you don't need an additional array for storage.
I have found an answer. So originally my problem was trying to spawn multiple, different-looking enemies, at random. I realized that I could solve the same issue by changing the texture of the Enemy, instead of creating many different Enemy Nodes. In order to spawn enemies at random with an array of textures, it would look something like this:
var enemy1 = SKTexture(imageNamed: "Enemy1")
var enemy2 = SKTexture(imageNamed: "Enemy2")
var enemy3 = SKTexture(imageNamed: "Enemy3")
var enemy4 = SKTexture(imageNamed: "Enemy4")
var enemy5 = SKTexture(imageNamed: "Enemy5")
var enemy6 = SKTexture(imageNamed: "Enemy6")
let EnemyArray = [Enemy1, Enemy2, Enemy3, Enemy4, Enemy5, Enemy6]
let randomElement = EnemyArray.randomElement()!
let enemy = SKSpriteNode(imageNamed: "")
enemy.name = "Enemy"
enemy.texture = randomElement
enemy.size = CGSize(width: 30, height: 30)
enemy.zPosition = 2
self.addChild(enemy)
var moveEnemy = SKAction.moveTo(y: -800, duration: 4.0)
let deleteEnemy = SKAction.removeFromParent()
let EnemySequence = SKAction.sequence([moveEnemy, deleteEnemy])
enemy.run(EnemySequence)
}
func runEnemy() {
run(SKAction.repeatForever(SKAction.sequence([SKAction.run(spawnEnemy), SKAction.wait(forDuration: 2.0)])))
}
Thanks everyone for the help

Can't add a child to GameScene, only to a SKSpriteNode

I am trying to create a SKSpriteNode from a class (Planets), to my GameScene, when a button is pressed. My problem is that the border doesn't appear on screen.
class Planets: SKSpriteNode, EventListenerNode, InteractiveNode {
var gameScene: GameScene!
func didMoveToScene(){
isUserInteractionEnabled = true
gameScene = (SKScene(fileNamed: "GameScene") as! GameScene)
}
func interact() {
print("hi \(self.name ?? "Planet X")")
let border = SKSpriteNode(color: SKColor.gray, size: CGSize(width: 200, height: 200))
border.name = "border"
border.zPosition = 100
border.position = CGPoint(x: gameScene.frame.size.width/2, y: gameScene.frame.size.height/2)
gameScene.addChild(border)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
interact()
}
}
However, if I use self.addChild,(self meaning the Planets class) it works, so it works if I add the border to a SKSpriteNode, but I don't want that because the size differ, the position differ too. I just want to add it to my GameScene.
Thank you in advance!
I think the problem here is that the Planets class is creating its own copy of GameScene with this line here:
gameScene = (SKScene(fileNamed: "GameScene") as! GameScene)
That copy is never presented and so it's never visible and neither is the border that was added to it.
If Planets is a child of GameScene then I would try referencing the parent of the Planets node when you add the border like this:
func interact() {
print("hi \(self.name ?? "Planet X")")
guard let gameScene = self.parent as? GameScene else {
print("Error: could not get the parent scene")
return
}
let border = SKSpriteNode(color: SKColor.gray, size: CGSize(width: 200, height: 200))
border.name = "border"
border.zPosition = 100
border.position = CGPoint(x: gameScene.frame.size.width/2, y: gameScene.frame.size.height/2)
gameScene.addChild(border)
}

No collisions being detected in subclassed nodes

I'm writing a game. It is not detecting any collisions, even though I can see the physics bodies as I have that view turned on.
To set-up the physics world in the game scene, I've coded the following above the class declaration:
struct PhysicsCategory {
static let None: UInt32 = 0
static let Chicken: UInt32 = 0b1
static let Edge: UInt32 = 0b10
}
Then, within the actual class of the scene:
override func didMove(to view: SKView) {
setupNodes()
setupTrial()
let playableRect = CGRect(x: 0, y: 0, width: size.width/2, height: size.height/2)
self.physicsBody = SKPhysicsBody(edgeLoopFrom: playableRect)
self.physicsWorld.contactDelegate = self
self.physicsBody!.categoryBitMask = PhysicsCategory.Edge
self.physicsBody!.contactTestBitMask = PhysicsCategory.Chicken
// This is important for handling all the custom events
enumerateChildNodes(withName: "//*", using: { node, _ in
// we need to limit this to chickens only
if let customNode = node as? CustomNodeEvents {
customNode.didMoveToScene()
}
})
}
Here is the code to detect collisions:
func didBegin(_ contact: SKPhysicsContact) {
print("something happened")
let collision = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
if collision == PhysicsCategory.Chicken | PhysicsCategory.Edge {
print("it works!")
}
}
The nodes I'd like to animate are chickens. I want the game to detect when they collide with the edges of the world above.
My chicken subclass is this:
class TargetNode: SKSpriteNode, CustomNodeEvents, InteractiveNode {
func didMoveToScene() {
isUserInteractionEnabled = true
anchorPoint = CGPoint(x: 0, y: 0)
let playableRect = CGRect(x: self.anchorPoint.x, y: self.anchorPoint.y, width: self.size.width, height: self.size.height)
physicsBody = SKPhysicsBody(edgeLoopFrom: playableRect)
physicsBody!.isDynamic = true
physicsBody!.categoryBitMask = PhysicsCategory.Chicken
physicsBody!.contactTestBitMask = PhysicsCategory.Edge | PhysicsCategory.Chicken
physicsBody!.velocity = CGVector(dx: 100, dy: 0)
}
}
EDIT: The nodes are being added to the scene using this method in the game scene file.
func generateItems(targetNumber: Int, target: Bool) {
let movingItems = true
for _ in 0...(targetNumber - 1) {
if (target) {
let name = createTarget()
let targetNode = TargetNode(imageNamed: name)
targetNode.name = name
fgNode.addChild(targetNode)
targetNode.position = generateRandomLocation()
//if movingItems { animateTargets(targetNode) }
}
}
setting your chicken to a SKPhysicsBody(edgeLoopFrom :_) is a bad idea. instead try SKPhysicsBody(rectangleOf :_) to draw a rectangle shape around your chicken, the type of shape you want to draw can vary, check the docs for more information.
Moreover to check weather the chickens have made contact with the playableRect
remove self.physicsBody!.contactTestBitMask = PhysicsCategory.Chicken because we don't want to check if the rect has made contact with the chicken we want to know if the chicken has made contact with the playable Rect. So keep contactTestBitMask on the chicken.
remove : | PhysicsCategory.Chicken from your chicken subclass, unless you want to check weather the chickens collide with each other as well.
lastly: check for collisions, if the chicken makes contact with the playableRect
func didBegin(_ contact: SKPhysicsContact) {
if contact.bodyA.categoryBitMask == PhysicsCategory.Chicken && contact.bodyB.categoryBitMask == PhysicsCategory.Edge {
print("Chickens have collided with edge")
}
}

Ending a game when two nodes collide

I have two separate nodes with their own physics bodies, and when they collide, an SKScene with the high score and replay button should present itself. This is how my scene is called:
func didBeginContact(contact: SKPhysicsContact) {
gameOver()
print("gameOver")
}
And this is how my physics bodies for my nodes are set up:
func createDown(position: CGPoint) -> SKNode {
let circleNode = SKSpriteNode()
let circle = SKSpriteNode(imageNamed: "first#2x")
circleNode.position = CGPointMake(position.x, position.y)
circleNode.physicsBody = SKPhysicsBody(circleOfRadius: 30)
circleNode.physicsBody?.dynamic = false
circle.size = CGSize(width: 75, height: 75)
circleNode.addChild(circle)
circleNode.name = "circleNode"
circle.name = "CIRCLE"
let up = SKAction.moveByX(0, y: -9000, duration: 100)
physicsBody?.categoryBitMask = blackCategory
circleNode.runAction(up)
return circleNode
}
func playerPhysics() {
player.physicsBody = SKPhysicsBody(circleOfRadius: 30)
player.physicsBody?.affectedByGravity = false
player.physicsBody?.categoryBitMask = playerCategory
player.physicsBody?.contactTestBitMask = blackCategory
}
And here is my gameOver function:
func gameOver() {
gameEnd = true
let reveal = SKTransition.fadeWithDuration(1)
let scene = GameOver(size: self.scene!.size)
view!.presentScene(scene, transition: reveal)
}
Am I missing something? Will post more code if necessary.
Just use intersectsNode and call gameOver() when the two nodes collide.
if yourNode.intersectsNode(yourSecondNode) {
gameOver()
}
SKNode Class Reference: SKNode
You need to add the physics world as a contact delegate for your methods to work.
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMoveToView(view: SKView) {
physicsWorld.contactDelegate = self
And like Whirlwind said, you need to set your categoryBitMask and contactTestBitMask for the circle node.
Then everything should work. I hope I could help.