Swift: Detect when SKSpriteNode has even touched - ios

I have a game where an object falls from the top of the screen to the bottom randomly. I want to have it to where if the object is tapped, I can tell it to do something, like add points to the score.
Can you help me try to figure this out? I'll be on to answer any questions if you happen to have any. Here is the code I'm using:
class GameSceneTest: SKScene, SKPhysicsContactDelegate {
override func didMoveToView(view: SKView) {
backgroundColor = SKColor.clearColor()
physicsWorld.gravity = CGVectorMake(0, 0)
physicsWorld.contactDelegate = self
//Change duration
runAction(SKAction.repeatActionForever(SKAction.sequence([SKAction.runBlock(addObject), SKAction.waitForDuration(1)])
))
//AddMusicHere
}
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
func add Object() {
let Object = SKSpriteNode(imageNamed: "Object\(arc4random_uniform(6) + 1).png")
Object.userInteractionEnabled = true
Object.size = CGSize(width: 50, height: 50)
Object.physicsBody = SKPhysicsBody(rectangleOfSize: Object.size)
Object.physicsBody?.dynamic = true
//Determine where to spawn gem across y axis
let actually = random(min: Object.size.width/2, max: size.width - Object.size.width/2)
//Position Object slightly off screen along right edge
//and along random y axis point
//Object.position = CGPoint(x: size.width + Object.size.width/2 , y: actually)
Object.position = CGPoint(x: actually, y: size.height + Object.size.height/2)
//Add the Object to the scene
addChild(Object)
//Determines speed of Object (edit later to where speed depends on type of Object)
let actualDuration = random(min: CGFloat(4), max: CGFloat(5))
//Create the Actions
let actionMove = SKAction.moveTo(CGPoint(x: actually, y: gem.size.height/2), duration: NSTimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
let loseAction = SKAction.runBlock() {
let reveal = SKTransition.flipHorizontalWithDuration(0.5) //Change to flipe vertical
let gameOverScene = GameOverScene(size: self.size, won: false)
self.view?.presentScene(gameOverScene, transition: reveal)
}
Object.runAction(SKAction.sequence([actionMove, loseAction, actionMoveDone]))
}

Take a look at "Moving The Cards Around The Board" in the following tutorial: Ray Wenderlicht SpriteKit Tutorial.
Then read a bit more around handling touch events in iOS (e.g. Handling Touches...).
Then try something in code and if you get stuck post a more specific question - because this one is a little too general.

Related

How to add to a variable in SpriteKit

Relatively new to swift so apologies if I'm being stupid.
I'm using Xcode 13.3 beta 3
I have some code which spawns in a sprite named Monster and moves it from the right of the screen to the left at random speeds and at a random y location.
When it moves off screen it removes from parent and transitions to a loose screen.
what I want it to do when it leaves the screen is for it to add to a variable I've defined at the top of the class:
var pass = 0
here's the code for the monsters:
func addMonster() {
let monster = SKSpriteNode(imageNamed: "monster")
monster.physicsBody = SKPhysicsBody(rectangleOf: monster.size)
monster.physicsBody?.isDynamic = true
monster.physicsBody?.categoryBitMask = PhysicsCatagory.monster
monster.physicsBody?.contactTestBitMask = PhysicsCatagory.projectile
monster.physicsBody?.collisionBitMask = PhysicsCatagory.none
let actualY = random(min: monster.size.height/2, max: size.height - monster.size.height/2)
monster.position = CGPoint(x: size.width + monster.size.width/2, y: actualY)
addChild(monster)
let actualDuration = random(min: CGFloat(1.65), max: CGFloat(5.5))
let actionMove = SKAction.move(to: CGPoint(x: -monster.size.width/2, y: actualY), duration: TimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
let loseAction = SKAction.run() { [weak self] in
guard let `self` = self else {return}
let end = SKTransition.crossFade(withDuration: 0.5)
let gameOverScene = GameOverScene(size: self.size, won: false)
self.view?.presentScene(gameOverScene, transition: end)
}
monster.run(SKAction.sequence([actionMove,loseAction, actionMoveDone]))
}
what I want is the loose action to just add +1 to the pass variable then have an if statement for when pass is greater than 3 to run the code that goes to the loose screen.
Hope that all makes sense
any help would be much appreciated!

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]))
}
}
""

Trying to animate a SKNode inside a UIView

I have programmatically created a SKView inside of my UIView. Now I want to animate a new SKNodes in all directions, starting in the middle (this works) depending on the view that is passed through. All works fine except for the fact that the ending position of the SKNode is weird. Instead of shooting in all directions, it is going inside of a corner, way out of the view's boundaries. It should never go out of the view's boundaries. I am converting the CGPoint to my scene from the View.
This is my code:
func animateExplosion(sender: UIButton){
let star = SKSpriteNode(imageNamed: "ExplodingStar")
let starHeight = sender.frame.height / 3
star.size = CGSize(width: starHeight, height: starHeight)
var position = sender.frame.origin
position = self.sceneScene.convertPoint(fromView: position)
star.position = position
let minimumDuration = 1
let maximumDuration = 2
let randomDuration = TimeInterval(RandomInt(min: minimumDuration * 100, max: maximumDuration * 100) / 100)
let fireAtWill = SKAction.move(to: getRandomPosition(view: sender), duration: randomDuration)
let rotation = SKAction.rotate(byAngle: CGFloat(randomAngle()), duration: Double(randomDuration))
let fadeOut = SKAction.fadeOut(withDuration: randomDuration)
let scaleTo = SKAction.scale(to: starHeight * 2, duration: randomDuration)
let group = SKAction.group([fireAtWill, rotation]) //for testing no fade out or remove parent
let sequence = SKAction.sequence([group])
if randomAmountOfExplodingStars > 0{
randomAmountOfExplodingStars -= 1
sceneScene.addChild(star)
star.run(sequence)
animateExplosion(sender: sender)
}
}
the getRandomPosition where the bug properly is:
func getRandomPosition(view: UIView) -> CGPoint{
let direction = RandomInt(min: 1, max: 4)
var randomX = Int()
var randomY = Int()
if direction == 1{
randomX = Int(view.frame.width / 2)
randomY = RandomInt(min: -Int(view.frame.height / 2), max: Int(view.frame.height / 2))
}
if direction == 2{
randomX = RandomInt(min: -Int(view.frame.width / 2), max: Int(view.frame.width / 2))
randomY = Int(view.frame.height / 2)
}
if direction == 3{
randomX = -Int(view.frame.width / 2)
randomY = RandomInt(min: -Int(view.frame.height / 2), max: Int(view.frame.height / 2))
}
if direction == 4{
randomX = RandomInt(min: -Int(view.frame.width / 2), max: Int(view.frame.width / 2))
randomY = -Int(view.frame.height / 2)
}
var randomPosition = CGPoint(x: randomX, y: randomY)
//randomPosition = self.sceneScene.convertPoint(fromView: randomPosition)
return randomPosition
}
I know that code looks awful, but it should do the trick right? The passed through view is a UIButton inside of a UIView. The SKView shares exactly the same constrains as that UIView. The animation should start in the middle and end somewhere to the boundaries of the passed view.
Yay got it to work finally. So dumb I did not notice before. The sender would be always a child inside of a UIView which is smaller. The return function was great, but the SKNode should not move to the returned function, but moved by.
Updated code:
let randomPositionOfSender = getRandomPosition(view: sender)
let fireAtWill = SKAction.moveBy(x: randomPositionOfSender.x, y: randomPositionOfSender.y, duration: randomDuration)

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.

Spawning a circle in a random spot on screen

I've been racking my brain and searching here and all over to try to find out how to generate a random position on screen to spawn a circle. I'm hoping someone here can help me because I'm completely stumped. Basically, I'm trying to create a shape that always spawns in a random spot on screen when the user touches.
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenHeight = screenSize.height
let screenWidth = screenSize.width
let currentBall = SKShapeNode(circleOfRadius: 100)
currentBall.position = CGPointMake(CGFloat(arc4random_uniform(UInt32(Float(screenWidth)))), CGFloat(arc4random_uniform(UInt32(Float(screenHeight)))))
self.removeAllChildren()
self.addChild(currentBall)
}
If you all need more of my code, there really isn't any more. But thank you for whatever help you can give! (Just to reiterate, this code kind of works... But a majority of the spawned balls seem to spawn offscreen)
The problem there is that you scene is bigger than your screen bounds
let viewMidX = view!.bounds.midX
let viewMidY = view!.bounds.midY
print(viewMidX)
print(viewMidY)
let sceneHeight = view!.scene!.frame.height
let sceneWidth = view!.scene!.frame.width
print(sceneWidth)
print(sceneHeight)
let currentBall = SKShapeNode(circleOfRadius: 100)
currentBall.fillColor = .green
let x = view!.scene!.frame.midX - viewMidX + CGFloat(arc4random_uniform(UInt32(viewMidX*2)))
let y = view!.scene!.frame.midY - viewMidY + CGFloat(arc4random_uniform(UInt32(viewMidY*2)))
print(x)
print(y)
currentBall.position = CGPoint(x: x, y: y)
view?.scene?.addChild(currentBall)
self.removeAllChildren()
self.addChild(currentBall)
First: Determine the area that will be valid. It might not be the frame of the superview because perhaps the ball (let's call it ballView) might be cut off. The area will likely be (in pseudocode):
CGSize( Width of the superview - width of ballView , Height of the superview - height of ballView)
Once you have a view of that size, just place it on screen with the origin 0, 0.
Secondly: Now you have a range of valid coordinates. Just use a random function (like the one you are using) to select one of them.
Create a swift file with the following:
extension Int
{
static func random(range: Range<Int>) -> Int
{
var offset = 0
if range.startIndex < 0 // allow negative ranges
{
offset = abs(range.startIndex)
}
let mini = UInt32(range.startIndex + offset)
let maxi = UInt32(range.endIndex + offset)
return Int(mini + arc4random_uniform(maxi - mini)) - offset
}
}
And now you can specify a random number as follows:
Int.random(1...1000) //generate a random number integer somewhere from 1 to 1000.
You can generate the values for the x and y coordinates now using this function.
Given the following random generators:
public extension CGFloat {
public static var random: CGFloat { return CGFloat(arc4random()) / CGFloat(UInt32.max) }
public static func random(between x: CGFloat, and y: CGFloat) -> CGFloat {
let (start, end) = x < y ? (x, y) : (y, x)
return start + CGFloat.random * (end - start)
}
}
public extension CGRect {
public var randomPoint: CGPoint {
var point = CGPoint()
point.x = CGFloat.random(between: origin.x, and: origin.x + width)
point.y = CGFloat.random(between: origin.y, and: origin.y + height)
return point
}
}
You can paste the following into a playground:
import XCPlayground
import SpriteKit
let view = SKView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
XCPShowView("game", view)
let scene = SKScene(size: view.frame.size)
view.presentScene(scene)
let wait = SKAction.waitForDuration(0.5)
let popIn = SKAction.scaleTo(1, duration: 0.25)
let popOut = SKAction.scaleTo(0, duration: 0.25)
let remove = SKAction.removeFromParent()
let popInAndOut = SKAction.sequence([popIn, wait, popOut, remove])
let addBall = SKAction.runBlock { [unowned scene] in
let ballRadius: CGFloat = 25
let ball = SKShapeNode(circleOfRadius: ballRadius)
var popInArea = scene.frame
popInArea.inset(dx: ballRadius, dy: ballRadius)
ball.position = popInArea.randomPoint
ball.xScale = 0
ball.yScale = 0
ball.runAction(popInAndOut)
scene.addChild(ball)
}
scene.runAction(SKAction.repeatActionForever(SKAction.sequence([addBall, wait])))
(Just make sure to also paste in the random generators, too, or to copy them to the playground's Sources, as well as to open the assistant editor so you can see the animation.)

Resources