I'm writing an isometric game using SpriteKit and swift, and I'm having trouble with sprites for my sheep. They have a basic AI that randomly moves them around, but when it draws I get two big black horizontal lines instead of a sheep. I'm using init(rect:inTexture:) to take the first sub-image of my sprite sheet. I'm only doing one sheep to test my code. Here's my code:
//
// Animal.swift
// Anointed
//
// Created by Jacob Jackson on 4/26/15.
// Copyright (c) 2015 ThinkMac Innovations. All rights reserved.
//
import Foundation
import SpriteKit
/* DEFINES AN ANIMAL */
class Animal : SKSpriteNode {
let animalName: String //name
let descript: String //description
let spriteSheetName: String //image name for item icon
var deltaT = 0.0
var startTime = 0.0
init( name: String, desc: String, sheetName: String ) {
animalName = name
descript = desc
spriteSheetName = sheetName
let texture = SKTexture(rect: CGRect(x: 0, y: 0, width: 32, height: 32), inTexture: SKTexture(imageNamed: spriteSheetName)) //make a texture for the animal's initial state
super.init(texture: texture, color: SKColor.clearColor(), size: texture.size()) //sets up tex, bgcolor, and size
}
func updateAI( time: CFTimeInterval ) {
if startTime == 0.0 {
startTime = time
} else {
deltaT = time - startTime
}
if deltaT > 2 {
moveRandom()
deltaT = 0.0
startTime = time
}
}
func moveRandom() {
var direction = random() % 4
switch( direction ) {
case 0: moveUP()
break
case 1: moveRT()
break
case 2: moveDN()
break
case 3: moveLT()
break
default:
break
}
}
func moveUP() {
let moveUp = SKAction.moveByX(0, y: 64, duration: 1.0)
self.runAction(moveUp)
}
func moveRT() {
let moveRt = SKAction.moveByX(32, y: 0, duration: 1.0)
self.runAction(moveRt)
}
func moveLT() {
let moveLt = SKAction.moveByX(-32, y: 0, duration: 1.0)
self.runAction(moveLt)
}
func moveDN() {
let moveDn = SKAction.moveByX(0, y: -64, duration: 1.0)
self.runAction(moveDn)
}
/* FROM APPLE. APPARENTLY NECESSARY IF I'M INHERITING FROM SKSpriteNode */
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then, to create the one sheep for a test:
var sheep1 = Animal(name: "Sheep", desc: "A White Sheep", sheetName: "whiteSheep")
var CaveStable = Location(n:"Cave Stable", d:"A small stable inside a cave, near an Inn", g:tempGrid, a:[sheep1]) //uses temporary grid defined above as the "Cave Stable" location where Jesus is born
Then, to place the sheep randomly based on an array of animals for each "location" (like a level):
for x in 0...theGame.currentLocation.animals.count-1 {
var animal = theGame.currentLocation.animals[x]
var pos = twoDToIso(CGPoint(x: (random() % (theGame.currentLocation.grid.count-1))*64, y: (random() % (theGame.currentLocation.grid[0].count-1))*64))
animal.position = CGPoint(x: pos.x, y: pos.y + animal.size.height / 2)
world.addChild(animal)
}
Then, in my scene code:
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
theGame.currentLocation.animals[0].updateAI(currentTime)
/* other stuff below here */
My whiteSheep sprite sheet looks like this:
Finally, this is what it looks like when the game is running:
The black lines move randomly, like the sheep should be doing - but what is going on with the graphics? Weirdness. Anybody have an idea what's going on?
The initialiser rect:inTexture: expects unit coordinates (0-1). So that line of code should be:
let spriteSheet = SKTexture(imageNamed: spriteSheetName)
let texture = SKTexture(rect: CGRect(x: 0, y: 0, width: 32/spriteSheet.size().width, height: 32/spriteSheet.size().height), inTexture: spriteSheet)
Related
It is pretty easy to get ready the following task in the old-school swift: every three seconds a new view (subview) appears in a new position. Here is a code:
import UIKit
class ViewController: UIViewController {
var someView = UIView()
var posX : CGFloat = 10
var posY : CGFloat = 10
var timer:Timer!
var loopCount = 1
override func viewDidLoad() {
super.viewDidLoad()
startTimer()
view.backgroundColor = .purple
}
func setView() {
someView = UIView(frame: CGRect(x: posX, y: posY, width: 10, height: 10))
someView.backgroundColor = UIColor.orange
view.addSubview(someView)
}
func startTimer() {
if timer == nil {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(loop), userInfo: nil, repeats: true)
}
}
#objc func loop(){
if (loopCount % 3 == 0) {
posX += 15
posY += 15
setView()
}
loopCount += 1
}
}
SwiftUI makes many things much easier, but not this one, I’m afraid. At least I could not find an easy way to solve it until now. Has somebody any idea?
Here is screen with the result (after several seconds):
Here is possible approach (tested with Xcode 11.2 / iOS 13.2). SwiftUI is reactive concept, so instead of add view itself, it is added a new position into view model and SwiftUI view in response to this change in view model refresh itself addition new view (in this case Rectangle) at new added position.
Demo (moment of start recording is not accurate, but rects added regularly):
Code: (see also some comments inline)
// needed to use as ID in ForEach
extension CGPoint: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(self.x)
hasher.combine(self.y)
}
}
// View model holding and generating new positions
class DemoViewModel: ObservableObject {
#Published var positions = [CGPoint]() // all points for view
private var loopCount = 0
func loop() {
if (loopCount % 3 == 0) {
if let last = positions.last { // generate new point
positions.append(CGPoint(x: last.x + 15, y: last.y + 15))
} else {
positions.append(CGPoint(x: 10, y: 10))
}
}
loopCount += 1
}
}
struct DemoAddingView: View {
#ObservedObject var vm = DemoViewModel()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
ForEach(vm.positions, id: \.self) { position in
Rectangle().fill(Color.orange) // just generate a rect view for all points
.frame(width: 10, height: 10)
.position(position) // location of rect in global coordinates
}
.onReceive(timer) { _ in
self.vm.loop() // add next point
}
}
}
}
I am trying to get my nodes to move across the screen and spawn infinitely on a timer. I would aslo like to have multiple nodes on screen at once. This is my stable code so far. I have tried multiple different ways with either freezes or crashes.
let bunnyTexture = SKTexture(imageNamed: "oval 1.png")
bunny = SKSpriteNode(texture: bunnyTexture)
bunny.position = CGPoint(x: (size.width / 3), y: 750 + bunny.frame.height)
self.addChild(bunny)
bunny.physicsBody = SKPhysicsBody(circleOfRadius: bunnyTexture.size().height/2)
bunny.physicsBody!.dynamic = true
let spawnBunny = SKAction.moveByX(0, y: -self.frame.size.height, duration: 4)
let despawnBunny = SKAction.removeFromParent()
let spawnNdespawn2 = SKAction.sequence([spawnBunny, despawnBunny])
bunny.runAction(spawnNdespawn2)
let gobblinTexture = SKTexture(imageNamed: "oval 2.png")
gobblin = SKSpriteNode(texture: gobblinTexture)
gobblin.position = CGPoint(x: 150 + gobblin.frame.width, y: (size.height / 3))
self.addChild(gobblin)
let randomGob = arc4random() % UInt32(self.frame.size.height / 2)
let spawnGob = SKAction.moveByX(+self.frame.size.width, y: 0, duration: 4)
let despawnGob = SKAction.removeFromParent()
let spawnNdespawn = SKAction.sequence([spawnGob, despawnGob])
gobblin.runAction(spawnNdespawn)
let ground = SKNode()
ground.position = CGPointMake(0, 0)
ground.physicsBody = SKPhysicsBody(rectangleOfSize: CGSizeMake(self.frame.size.width, -50))
ground.physicsBody!.dynamic = false
self.addChild(ground)
You can use SKAction.waitForDuration(interval) to create a looping timer.
Extend SKNode with a new function loopAction. This takes a SKAction an NSTimeInterval and a () -> Bool function.
The SKAction is the function that will be executed at each interval.
The () -> Bool function will be used to stop the otherwise infinite loop.
Just remember that this captures both the SKNode and the SKAction. Neither will deallocate until the loop is stopped.
Its possible to create an object with a flag (Bool) to hold all this information and set the flag to false when it needs to stop. I just prefer this way.
LoopActionManager is an example of how the loopAction is implemented.
You need an SKNode and an SKAction. Instead of calling runAction on the SKNode you call loopAction and pass the interval and the function that controls when to stop. You would place this code somewhere you have access to the nodes in question.
The function to stop can also be implemented as a trailing closure
Paste the code into a playground to see how it works.
import UIKit
import SpriteKit
import XCPlayground
extension SKNode {
func loopAction(action:SKAction,interval:NSTimeInterval,continueLoop:() -> Bool) {
guard continueLoop() else { return }
runAction(SKAction.waitForDuration(interval)) {
if continueLoop() {
self.runAction(action)
self.loopAction(action, interval: interval, continueLoop: continueLoop)
}
}
}
}
// example
class LoopActionManager {
let node = SKSpriteNode(color: UIColor.whiteColor(), size: CGSize(width: 20, height: 20))
let action = SKAction.moveBy(CGVector(dx: 10,dy: 0), duration: 0.5)
func continueMoveLoop() -> Bool {
return (node.position.x + node.size.width) < node.scene?.size.width
}
func start() {
node.loopAction(action, interval: 1, continueLoop: continueMoveLoop)
}
}
let view = SKView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
let scene = SKScene(size: view.frame.size)
view.presentScene(scene)
let example = LoopActionManager()
scene.addChild(example.node)
example.start()
XCPlaygroundPage.currentPage.liveView = view
As stated in the comments below it is also possible to use repeatAction.
You can extend SKAction with a static func to generate the repeatAction based on an action to repeat and an interval.
In comparison to the first solution this is simpler and more in line with the SDK. You do lose the completion handler for each interval.
import UIKit
import SpriteKit
import XCPlayground
extension SKAction {
static func repeatAction(action:SKAction,interval:NSTimeInterval) -> SKAction {
// diff between interval and action.duration will be the wait time. This makes interval the interval between action starts. Max of 0 and diff to make sure it isn't smaller than 0
let waitAction = SKAction.waitForDuration(max(0,interval - action.duration))
let sequenceAction = SKAction.sequence([waitAction,action])
let repeatAction = SKAction.repeatActionForever(sequenceAction)
return repeatAction
}
}
let view = SKView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
let scene = SKScene(size: view.frame.size)
view.presentScene(scene)
let node = SKSpriteNode(color: UIColor.whiteColor(), size: CGSize(width: 20, height: 20))
let action = SKAction.moveBy(CGVector(dx: 10,dy: 0), duration: 0.5)
scene.addChild(node)
node.runAction(SKAction.repeatAction(action, interval: 1.0))
XCPlaygroundPage.currentPage.liveView = view
Im new with Xcode and swift.
Im doing this game from this tutorial ( https://www.youtube.com/watch?v=hLOhirsXx9c&list=PL1YTxp2xLtqSiBhoAiaImXFcjAAxvo1es&index=1 )
The game is working good, but i wanna do that game with pictures, not code.
I can add background but i can't get pictures top of the background and get them moving.
example : this is the code for the ground that its moving to the right, but i wanna put 2 images instead of code.
import Foundation
import SpriteKit
class KBMovingGround: SKSpriteNode {
let NUMBER_OF_SEGMENTS = 20
let COLOR_ONE = UIColor(red: 88.0/255.0, green: 148.0/255.0, blue: 87.0/255.0, alpha: 1.0)
let COLOR_TWO = UIColor(red: 120.0/255.0, green: 195.0/255.0, blue: 118.0/225.0, alpha: 1.0)
init(size: CGSize) {
super.init(texture: nil, color: UIColor.brownColor(), size: CGSizeMake(size.width*2, size.height))
anchorPoint = CGPointMake(0, 0.5)
for var i = 0; i < NUMBER_OF_SEGMENTS; i++ {
var segmentColor: UIColor!
if i % 2 == 0 {
segmentColor = COLOR_ONE
} else {
segmentColor = COLOR_TWO
}
let segment = SKSpriteNode(color: segmentColor, size: CGSizeMake(self.size.width / CGFloat (NUMBER_OF_SEGMENTS), self.size.height))
segment.anchorPoint = CGPointMake(0.0, 0.5)
segment.position = CGPointMake(CGFloat(i)*segment.size.width, 0)
addChild(segment)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func start(){
let moveLeft = SKAction.moveByX(-frame.size.width/2, y: 0, duration: 1.0)
let resetPosition = SKAction.moveToX(0, duration: 0)
let moveSequence = SKAction.sequence([moveLeft, resetPosition])
runAction(SKAction.repeatActionForever(moveSequence))
}
}
And i wanna do same thing with that "ninja" moving top on the ground.
Then i wanna make the ground and the objective thats moving on it go for the same speed so how do i make that?
And how i can make it speeding up when the player gets further at the game.
Thank you for the help beforehand and sorry for my bad english!
I have created a rectangular image in the form of an SKSpriteNode in Swift with the following code:
var screenImage = SKSpriteNode(texture: SKTexture(imageNamed: "\(imageChoices[randomImageChoice].0)"))
screenImage.position = CGPointMake(screen1.position.x, screen1.position.y)
screenImage.size = CGSizeMake(self.frame.size.width * 0.6, self.frame.size.height)
self.addChild(screenImage)
I proceed to move the image with the following code:
func swipedTrue(sender: UISwipeGestureRecognizer) {
if gameOver == false && tutorial == false {
//if you swipe, it checks if you were right, then moves on or GameOver()
if (wordChoices[randomWordChoice]).1 == true {
//reset time
timePerQuestion = 1.0
//randomize word
randomWordChoice = Int(arc4random() % 3)
newImage = SKSpriteNode(texture: SKTexture(imageNamed: "\(wordChoices[randomWordChoice].0)"))
//randomize color of screens, mid-swipe
newScreen.fillColor = UIColor(red: CGFloat(drand48()), green: CGFloat(drand48()), blue: CGFloat(drand48()), alpha: 1.0)
//replace timeBar
decreaseTimeBlock.fillColor = newScreen.fillColor
decreaseTimeBlock.position = CGPointMake(self.frame.size.width * 1.5, self.frame.size.height * 0.985)
timeBarRedValue = 0.0; timeBarGreenValue = 1.0
newTimeBar.fillColor = UIColor(red: CGFloat(timeBarRedValue), green: CGFloat(timeBarGreenValue), blue: 0.0, alpha: 1.0)
//actions caused by swipe: it's "bringNewScreen" because if you swipeFalse, the newScreen comes from bottom. If you swipeTrue, it comes from the top.
var swipeTrueCurrentScreen = SKAction.moveToX(self.frame.size.width * 2, duration: 0.5)
var bringNewScreen = SKAction.moveToY(self.frame.size.height * 0.5, duration: 0.5)
var bringNewTimeBar = SKAction.moveToY(self.frame.size.height * 0.985, duration: 0.5)
//reset the newScreen and word to the top of the screen, to be dropped again
newScreen.position = CGPointMake(self.frame.size.width * 0.5, self.frame.size.height * 1)
newImage.position = CGPointMake(self.frame.size.width * 0.5, self.frame.size.height * 1)
newTimeBar.position = CGPointMake(self.frame.size.width * 0.5, self.frame.size.height * 1.58)
//swipe word and screen
currentImage.runAction(swipeTrueCurrentScreen)
currentTimeBar.runAction(swipeTrueCurrentScreen)
currentScreen.runAction(swipeTrueCurrentScreen)
//make swiping noise
runAction(SKAction.playSoundFileNamed("Swoosh 3-SoundBible.com-1573211927.mp3", waitForCompletion: false))
//bring in the newScreen
newScreen.runAction(bringNewScreen)
newImage.runAction(bringNewScreen)
newTimeBar.runAction(bringNewTimeBar)
//increase score
++score
scoreLabel.text = "\(score)"
//here, switch the currentScreen with the newScreen so that the process can be repeated
if newScreen == screen1 {
newScreen = screen2
newImage = screenImage2
newTimeBar = timeBar2
currentScreen = screen1
currentImage = screenImage1
currentTimeBar = timeBar1
} else {
newScreen = screen1
newImage = screenImage1
newTimeBar = timeBar1
currentScreen = screen2
currentImage = screenImage2
currentTimeBar = timeBar2
}
} else {
GameOver()
}
}
}
However, for some reason, the image will not move, and when I try to move it in other situations at well, it refuses. How can I fix this?
Except one missing parenthesis here (but I guess it's not the case in your code) the code have no particular reason to not work. The problem is most likely on how you use it.
My guess is that you are doing something like so :
import SpriteKit
class GameScene: SKScene {
var sprite : SKSpriteNode = SKSpriteNode() // A
override func didMoveToView(view: SKView) {
// You are creating another `sprite` variable
// and not using the (A) sprite you declare above
var sprite = SKSpriteNode(imageNamed:"Spaceship") // B
// Here you set the (B) sprite you just created
sprite.size = CGSizeMake(self.frame.size.width * 0.6, self.frame.size.height)
sprite.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame));
// Here it's still the (B) sprite you just created
// that you add to the scene
self.addChild(sprite)
// You are calling your action from somewhere else
self.applyAction()
}
func applyAction() {
// You create an action, OK
let action = SKAction.moveToX(self.frame.size.width * 2, duration: 0.5)
// You apply the action to the (A) sprite property you have in your class
// Same as : self.sprite.runAction(action)
sprite.runAction(action)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
In that case, you just have to not create another sprite variable. By removing the var keyword on the // B line.
Let met know if it helped.
If it's not the case, please give more details (code, ...).
I'm creating a series of moving pipes in my scene. But it always crashes after ~30 pipes are generated. Is it because of too many nodes in the scene and no memories for new ones? The code is like this:
import SpriteKit
class GameScene: SKScene {
var mainPipe: SKSpriteNode = SKSpriteNode()
var space:Float = 1000
var pipeCount:Int = 0
override func didMoveToView(view: SKView) {
self.backgroundColor = SKColor.blackColor()
self.size.width = 640
self.size.height = 1136
}
func randomOffset() -> Float{
var rNum:Float = Float(arc4random()%181) // 0-180
return rNum
}
var durations: CFloat = 5.0
var colorPipes:UIColor = UIColor.grayColor()
func spawnPipeRow(offs:Float){
self.pipeCount = self.pipeCount + 1
println("\(self.pipeCount)")
//offs is the random number
//let offset = offs + (space/2) - 105
let offset = offs + Float(self.size.height/100) - 180
// mainPipe = SKSpriteNode(color:colorPipes, size:CGSize(width: view.bounds.size.width/3, height:700))
mainPipe = SKSpriteNode(color:colorPipes, size:CGSize(width: self.size.width/5, height:self.size.height/1.5))
let pipeBottom = (mainPipe as SKSpriteNode).copy() as SKSpriteNode
let pipeTop = (mainPipe as SKSpriteNode).copy() as SKSpriteNode
let xx = self.size.width * 2.0
self.setPositionRelativeBot(pipeBottom, x:Float(xx), y: offset )
self.setPositionRelativeTop(pipeTop, x:Float(xx), y: offset + space)
pipeBottom.physicsBody = SKPhysicsBody(rectangleOfSize: pipeBottom.size)
pipeTop.physicsBody = SKPhysicsBody(rectangleOfSize: pipeTop.size)
pipeBottom.physicsBody?.dynamic = false
pipeTop.physicsBody?.dynamic = false
//pipeTop.physicsBody?.contactTestBitMask = birdCategory
//pipeBottom.physicsBody?.contactTestBitMask = birdCategory
self.addChild(pipeBottom)
self.addChild(pipeTop)
var actionArray1:NSMutableArray = NSMutableArray()
actionArray1.addObject(SKAction.moveTo(CGPointMake(-1000, pipeBottom.size.height - 200), duration: NSTimeInterval(durations)))
var actionArray2:NSMutableArray = NSMutableArray()
actionArray2.addObject(SKAction.moveTo(CGPointMake(-1000, pipeTop.size.height - 200), duration: NSTimeInterval(durations)))
actionArray1.addObject(SKAction.removeFromParent())
actionArray2.addObject(SKAction.removeFromParent())
pipeBottom.runAction(SKAction.sequence(actionArray1))
pipeTop.runAction(SKAction.sequence(actionArray2))
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
}
}
override func update(currentTime: CFTimeInterval) {
var timeSinceLastUpdate = currentTime - lastUpdateTimerInterval
lastUpdateTimerInterval = currentTime
if(timeSinceLastUpdate > 1){
timeSinceLastUpdate = 1/60
lastUpdateTimerInterval=currentTime
}
updateWithTimeSinceLastUpdate(timeSinceLastUpdate)
/* Called before each frame is rendered */
}
func setPositionRelativeBot(node:SKSpriteNode, x: Float, y: Float){
let xx = (Float(node.size.width)/2) + x
let yy = (Float(self.size.height)/2) - (Float(node.size.height)/2) + y
node.position.x = CGFloat(xx)
node.position.y = CGFloat(yy)
}
func setPositionRelativeTop(node:SKSpriteNode, x:Float, y:Float){
let xx = (Float(node.size.width)/2) + x
let yy = (Float(self.size.height)/2) + (Float(node.size.height)/2) + y
node.position.x = CGFloat(xx)
node.position.y = CGFloat(yy)
}
var lastUpdateTimerInterval:NSTimeInterval = NSTimeInterval()
var lastYieldTimeInterval:NSTimeInterval = NSTimeInterval()
var speedOfBird: CDouble = 1.8
func updateWithTimeSinceLastUpdate(timeSinceLastUpdate:CFTimeInterval){
lastYieldTimeInterval += timeSinceLastUpdate
if(lastYieldTimeInterval > speedOfBird ){
lastYieldTimeInterval=0
self.spawnPipeRow(self.randomOffset())
if speedOfBird > 0.8{
speedOfBird -= 0.1}
}
}
}
You should remove your sprites from the scene when you no longer need them. However your problem is probably not related to the memory occupied by your textures:
SpriteKit Programming Guide
An SKTexture object is created and attached to the sprite. This
texture object automatically loads the texture data whenever the
sprite node is in the scene, is visible, and is necessary for
rendering the scene. Later, if the sprite is removed from the scene or
is no longer visible, Sprite Kit can delete the texture data if it
needs that memory for other purposes. This automatic memory management
simplifies but does not eliminate the work you need to do to manage
art assets in your game.
The texture object itself is just a placeholder for the actual texture
data. The texture data is more resource intensive, so Sprite Kit loads
it into memory only when needed.
If you already have an SKTexture object, you can create new textures
that reference a portion of it. This approach is efficient because the
new texture objects reference the same texture data in memory.
Try to delete them with this code :
override func update(currentTime: CFTimeInterval) {
self.enumerateChildNodesWithName("nodeName") {
node, stop in
if (node is SKSpriteNode) {
let sprite = node as SKSpriteNode
// Check if the node is not in the scene
if (sprite.position.x < -sprite.size.width/2.0 || sprite.position.x > self.size.width+sprite.size.width/2.0
|| sprite.position.y < -sprite.size.height/2.0 || sprite.position.y > self.size.height+sprite.size.height/2.0) {
sprite.removeFromParent()
println("outside")
}
}
}
}
Don't forget to named your node :
node.name = "nodeName"
Hope your crash will stop