I've coded a simple game using Swift 4 and XCode, and I've coded everything in the GameScene. All my elements (the monsters, the player, the projectile, etc.) are coded in the GameScene.
I want to transfer my code into dedicated classes (Class Player, class monster, etc.)
I would like to know what the basic structure of a SKSpriteNode class and the call of that class in the GameScene, to be more efficient at adapting my code.
Here's an example of what I've tried :
class Vaisseau: SKSpriteNode /*: Creatures */{
var coeur: Int = 0
init(texture: SKTexture, size: CGSize)
{
let texture = SKTexture(imageNamed: "player")
super.init(texture: texture, color: UIColor.clear, size: texture.size())
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
And the initialization in the GameScene :
let player = Vaisseau()
Here's how it is actually defined in the GameScene :
let player = SKSpriteNode(imageNamed: "player")
you are declaring your init to have two parameters (texture: SKTexture, size: CGSize) but you are not passing the parameters in your initialization call
let player = Vaisseau()
you either need to change the initialization to...
let texture = SKTexture(imageNamed: "player")
let player = Vaisseau(texture: texture, size: texture.size())
and change the init to
init(texture: SKTexture, size: CGSize) {
super.init(texture: texture, color: UIColor.clear, size: size)
}
OR change the init in the class to...
init() {
let texture = SKTexture(imageNamed: "player")
super.init(texture: texture, color: UIColor.clear, size: texture.size())
}
and leave your initialization call as...
let player = Vaisseau()
player.position = CGPoint(x: 500, y: 500)
addChild(player)
EDIT added the above 2 lines to show you that those need to be in the scene
but other items such as alpha, zPosition, actions, zRotation etc. can be inside of the class
What you need to ask yourself to figure out which one to use is "will the texture for the player ever be different?" if so you may want to consider the first option where you pass in the texture.
Related
As I'm new to Swift iOS programming and I have question about creating "prefabs"?
I have Unity background and there was something like prefabs that you can instantiate many times. Is there anything similar to that in Swift or Xcode? Especially I'm looking for that in SpriteKit to create copy of SKNode with children
I think the closest thing to prefabs would be classes.
you can create a subclass of a SKnode or any other node, for example a SKSpriteNode and then setup that class to do the things you want. Then create an instance of that class.
class Box : SKSpriteNode{
init(){
//Setup your node the way you want it
super.init(texture: nil, color: UIColor.red, size: CGSize(width: 50, height: 50))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//create instance and add to scene.
let box = Box()
self.addChild(box)
you can also setup a node and then use the copy function to copy this node along with its children.
let block1 = SKSpriteNode(color: UIColor.red, size: CGSize(width: 50, height: 50))
let child = SKSpriteNode(color: UIColor.blue, size: CGSize(width: 25, height: 25))
block1.addChild(child)
let copy = block1.copy() as! SKSpriteNode
addChild(copy)
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
I'm trying to make infinite scrolling terrain by programmatically adding/deleting textured tiles as the player pans the view. New tiles should only be added next to existing tiles that have an open edge. To detect if a tile has an open edge, I plan to attach a small physics body that sticks out from all 4 sides of the tiles to act as a sensor. If the sensor contacts any other sensors, we know that edge of the tile is not open.
The problem I'm having is that the sensors do not always stay aligned with the tiles. To show this problem, I created a SpriteKit project with the code below.
Touch behavior includes a gesture recognizer in the GameScene class which causes the invisible Handle object to move. When the gesture ends, I use the handle's physics body to give it a little velocity on this line:
handle.physicsBody?.applyImpulse(CGVector(dx: velocity.x * multiplier, dy: -velocity.y * multiplier))
I'm also creating a Tile object (big green square below) and adding it as a child of the invisible handle. That's great, now all child tiles I add will move along with their parent handle.
Whenever a tile is instantiated, a Sensor object (small red square below) is created and added as a child of the tile. That's also great, now all sensors will move along with their parent tile which in turn moves with its parent, the invisible handle. There's just one problem...
When I pan the screen, both the green tile and its red sensor (shown below) move together in unison, as expected. When I release my pan gesture, the extra kick of velocity I give to the handle also carries over to its child tile, also as expected. But that velocity does not affect the child sensor of the tile. As soon as I release the gesture, the sensor stops dead on the screen while the tile continues moving along with the handle until they both slow to a halt. The desired behavior is for the sensor to keep moving along with its parent tile.
Here's a link to a video that might show what's happening better than I can describe it:
https://youtu.be/ccJKdZv-NsM
I can't understand why the tile is staying in sync with its parents motion but the sensor is not doing the same. Thanks for any insight into this problem.
GameScene.swift:
import SpriteKit
class GameScene: SKScene {
let handle = Handle()
let startTile = Tile()
override func didMove(to view: SKView) {
self.backgroundColor = .white
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
self.addChild(handle)
startTile.position.x = handle.anchorPoint.x
startTile.position.y = handle.anchorPoint.y
handle.addChild(startTile)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanFrom))
panGestureRecognizer.cancelsTouchesInView = false
panGestureRecognizer.delaysTouchesEnded = false
self.view!.addGestureRecognizer(panGestureRecognizer)
}
func handlePanFrom(_ recognizer: UIPanGestureRecognizer) {
if recognizer.state == .changed {
var translation = recognizer.translation(in: recognizer.view)
translation = CGPoint(x: translation.x, y: -translation.y)
self.panForTranslation(translation)
recognizer.setTranslation(.zero, in: recognizer.view)
} else if recognizer.state == .ended {
let velocity = recognizer.velocity(in: self.view)
let multiplier = CGFloat(0.5)
handle.physicsBody?.applyImpulse(CGVector(dx: velocity.x * multiplier, dy: -velocity.y * multiplier))
}
}
func panForTranslation(_ translation: CGPoint) {
let position = handle.position
let newPosition = CGPoint(x: position.x + translation.x, y: position.y + translation.y)
handle.position = newPosition
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
handle.physicsBody?.isResting = true
}
}
Handle class:
import SpriteKit
class Handle : SKSpriteNode {
init() {
super.init(texture: nil, color: .clear, size: CGSize(width: 1, height: 1))
self.physicsBody = SKPhysicsBody(rectangleOf: self.size)
self.physicsBody?.mass = 1
self.physicsBody?.linearDamping = 2
self.physicsBody?.categoryBitMask = 0
self.physicsBody?.contactTestBitMask = 0
self.physicsBody?.collisionBitMask = 0
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Tile class:
import SpriteKit
class Tile : SKSpriteNode {
init() {
super.init(texture: nil, color: .green, size: CGSize(width: 300, height: 300))
let sensorA = Sensor()
self.addChild(sensorA)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Sensor class:
import SpriteKit
class Sensor : SKSpriteNode {
init() {
super.init(texture: nil, color: .red, size: CGSize(width: 50, height: 50))
self.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 50, height: 50))
self.physicsBody?.categoryBitMask = 0b1
self.physicsBody?.contactTestBitMask = 0b1
self.physicsBody?.collisionBitMask = 0
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
UPDATE:
The accepted answer provided by Whirlwind solved the problem I had with the child separating from the parent. I believe that the cause of the problem became clear in the comments of that answer.
My understanding of it is that the red square did not move because it has its own physics body which is not receiving any velocity after the handle stops moving. While the handle object (and its child tile) keeps moving because it does have velocity. So it sounds like the red box's own physics body was holding it back.
I don't really have a time to get into why your code does some things, but if you want to move another physics body along with handle's physics body, then you could pin it to it.
I will just modify your code to make it work, but you should worry by yourself about encapsulation. First make a sensor variable inside of Tile class visible to the outside world, so you can use it later in your scene:
class Tile : SKSpriteNode {
let sensorA = Sensor()
init() {
super.init(texture: nil, color: .green, size: CGSize(width: 300, height: 300))
self.addChild(sensorA)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Then in you scene pin sensor to a handle:
let pin = SKPhysicsJointPin.joint(withBodyA: self.startTile.sensorA.physicsBody!, bodyB: self.handle.physicsBody!, anchor: CGPoint.zero)
startTile.sensorA.physicsBody?.allowsRotation = false
self.physicsWorld.add(pin)
I guess this is what you wanted:
Attempting to design a game using Sprite-Kit I found that it would be easier to create a seperate class for one of my game objects being a laser from a ship. Within that class I would have some functions maybe such as updating, shooting etc. But whenever I create another class none of the variables I make are "declared". This is my code
import UIKit
import SpriteKit
class laser: SKSpriteNode {
let laser : SKSpriteNode = SKSpriteNode(imageNamed:"playerShip")
laser.position = CGPoint(x: 100, y: 200)//This is where it says no declaration
}
This is because you need to initiate the class.
In your example you would need to do the following which would allow you instantiate a laser like this laser()
class laser: SKSpriteNode {
let laser: SKSpriteNode = SKSpriteNode(imageNamed: "playerShip")
init() {
super.init(texture: nil, color: .clear, size: laser.size)
laser.position = CGPoint(x: 100, y: 200)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
But you probably really wanted this, which allows you to instantiate a laser like this let aLaser = laser("playerShip"). Then you can change the position like this alaser.position = CGPoint(x: 100, y: 200).
This method allows you to change the sprite and position easily for different lasers. Unless your game only has one laser.
class laser: SKSpriteNode {
init(_ imageName: String) {
let texture: SKTexture = SKTexture(imageNamed: imageName)
super.init(texture: texture, color: .clear, size: texture.size())
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I'm struggling to understand how class initialisation works. From looking at solutions to other problems I have this as an example of the classes in my app.
import Foundation
import SpriteKit
class Ground : SKSpriteNode {
override init(texture: SKTexture!, color: SKColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
self.zPosition = -20
self.name = "Ground";
}
func configure (size: CGSize, position: CGPoint) {
self.size = size
self.position = position
// Set up the Physics
self.physicsBody = SKPhysicsBody(rectangleOf: size)
self.physicsBody?.contactTestBitMask = PhysicsCategory.Player
self.physicsBody?.categoryBitMask = PhysicsCategory.Ground
self.physicsBody?.collisionBitMask = PhysicsCategory.All
self.physicsBody?.affectedByGravity = false
self.physicsBody?.allowsRotation = false
self.physicsBody?.isDynamic = false
self.physicsBody?.mass = 1.99999
}
convenience init(color: SKColor, isActive: Bool = false) {
let size = CGSize(width: 10, height: 10);
self.init(texture:nil, color: color, size: size)
}
required init?(coder aDecoder: NSCoder) {
// Decoding length here would be nice...
super.init(coder: aDecoder)
}
}
I've put the 'configure' function as a kludge fix to let me pass the scene to the class so I can set sizes depending on the size of the device screen. Ideally I would like to just pull this information on initialisation but everything I try throws up errors that I don't understand.
Im not sure which way would be correct but I was wondering firstly, how would I pass arguments to the class to start with.. e.g..
let myClass = Ground(scene: self)
or can I somehow pull the scene information from directly within the class? I can pass info into functions/methods as I did with 'configure' but I couldn't get it to work on initialisation which would certainly be cleaner.
How would you guys do it?
Size
You should not programmatically change the size of a sprite depending on the current device, just use the same size and then let SpriteKit resizing it for you.
Loading from .sks
This initializer
init?(coder aDecoder: NSCoder)
is used when the Sprite is loaded from a .sks file. However in Xcode 7 you cannot pass values from the .sks to the sprite to set the attributes. I'm not sure you are using a .sks file so for now I am just throwing a fatal_error here. In Xcode 8 however you will be able to pass values from the .sks file to your own class.
required init?(coder aDecoder: NSCoder) {
fatalError("Not implemented")
}
Scene
You don't need to pass a reference of the current scene to your class. Every SKNode has the scene property, just use it to retrieve the scene where the node lives. Of course keep in mind that if you invoke .scene before the node has been added to a scene it will returns nil.
Code
This is the final code
class Ground : SKSpriteNode {
init(color: SKColor = .clearColor(), isActive:Bool = false) {
let texture = SKTexture(imageNamed: "ground")
super.init(texture: texture, color: color, size: texture.size())
self.name = "Ground"
self.zPosition = -20
let physicsBody = SKPhysicsBody(texture: texture, size: texture.size())
physicsBody.contactTestBitMask = PhysicsCategory.Player
physicsBody.categoryBitMask = PhysicsCategory.Ground
physicsBody.collisionBitMask = PhysicsCategory.All
physicsBody.affectedByGravity = false
physicsBody.allowsRotation = false
physicsBody.dynamic = false
physicsBody.mass = 1.99999
self.physicsBody = physicsBody
}
required init?(coder aDecoder: NSCoder) {
fatalError("Not implemented")
}
}
Unless I'm missing something else you're trying to do, you should be able to move the initialization of the PhysicsBody and the other properties from you configure() method and into your init() for the class. Then you can pre-size the texture you pass in on instantiation to result in the proper size for the device.
Also, the position property of the SKSpriteNode can be set by the instantiating class. This means you can accomplish the sizing based on device and the setting of the position property right after instantiating it in your scene and before adding it as a child of the scene.
ADDED:
class Ground : SKSpriteNode {
override init(texture: SKTexture!, color: SKColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
}
convenience init(texture: String, scene: SKScene, isActive: Bool = false) {
let texture = SKTexture(imageNamed: "\(yourtexturename)")
self.init(texture:teture, color: color, size: scene.size)
self.size = size
self.position = [YOU CAN NOW SET POSITION BASED ON THE SCENE]
self.physicsBody = SKPhysicsBody(rectangleOf: size)
self.physicsBody?.contactTestBitMask = PhysicsCategory.Player
self.physicsBody?.categoryBitMask = PhysicsCategory.Ground
self.physicsBody?.collisionBitMask = PhysicsCategory.All
self.physicsBody?.affectedByGravity = false
self.physicsBody?.allowsRotation = false
self.physicsBody?.isDynamic = false
self.physicsBody?.mass = 1.99999
self.zPosition = -20
self.name = "Ground";
}
required init?(coder aDecoder: NSCoder) {
// Decoding length here would be nice...
super.init(coder: aDecoder)
}
}
You might not need to override the original init() method but I didn't test anything.