SpriteKit: stuck trying to use SKNode.contains() on scaled sprites - swift

I'm having trouble using SKNode.contains() on scaled sprites, and I can't figure out what important concept I'm missing.
I have "gremlin" sprites that move around inside a "jail" sprite. Gremlins can go through walls sometimes, and I want to know when they've left the jail. I thought I could simply use jail.contains(gremlin) (or some variant), perhaps with a bit of math done on it to get the scale right, but no joy. I can't seem to figure out which sprite or which frame, or which size, or what kind of transform, or whatever, to use with contains().
I've read quite a bit about coordinate systems, frames and bounds, hit testing, scaling, frames, sizes, origins, anchor points, everything I've been able to find. I've read this SO question, and this, this, this, and this, and quite a few others.
I'm missing something. Here's some stripped-down code that shows the problem I'm having. This is the cleaned-up version. I've tried every permutation of convertPoint(), shrinking and growing size, shrinking and growing scale, changing who is whose parent, and various other desperate attempts to understand. No luck.
In this sample code, just to get my head around the problem, I'd like to control which of the big rectangles is to serve as the container for the small ones. The way it's set right now, I'm trying to get the little rectangles outside the red rectangle to be gray. As you can see, it seems that the red rectangle's hit-test area is the same size as that of the blue rectangle. That is, everything is green except the ones out at the very edges of the blue rectangle.
Help!
class GameScene: SKScene {
static var shared: GameScene?
var bottomSprite: SKSpriteNode!
var middleSprite: SKSpriteNode!
var setupComplete = false
var spriteTexture: SKTexture?
var textureAtlas: SKTextureAtlas?
var topSprite: SKSpriteNode!
func getMarkerColor(outerSprite: SKSpriteNode, innerSprite: SKSpriteNode) -> NSColor {
return outerSprite.frame.contains(innerSprite.frame) ? .green : .gray
}
override func didMove(to view: SKView) {
GameScene.shared = self
spriteTexture = SKTexture(imageNamed: "debugRectangle")
}
func drawContainerSprite(parent: SKNode, scale: CGFloat, color: NSColor) -> SKSpriteNode {
let sprite = SKSpriteNode(texture: spriteTexture)
sprite.color = color
sprite.colorBlendFactor = 1.0
sprite.anchorPoint = CGPoint(x: 0.5, y: 0.5)
sprite.setScale(scale)
sprite.size = scene!.size
parent.addChild(sprite)
sprite.position = CGPoint.zero
return sprite
}
func drawMarkerSprite(parent: SKNode, scale: CGFloat) -> SKSpriteNode {
let sprite = SKSpriteNode(texture: spriteTexture)
sprite.size = CGSize(width: bottomSprite.size.width * 0.05, height: bottomSprite.size.height * 0.05)
sprite.colorBlendFactor = 1.0
sprite.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let x = Int.random(in: Int(-self.bottomSprite.size.width)..<Int(self.bottomSprite.size.width))
let y = Int.random(in: Int(-self.bottomSprite.size.height)..<Int(self.bottomSprite.size.height))
sprite.position = CGPoint(x: x, y: y)
parent.addChild(sprite)
return sprite
}
override func update(_ currentTime: TimeInterval) {
if !setupComplete {
bottomSprite = drawContainerSprite(parent: self.scene!, scale: 0.5, color: .blue)
middleSprite = drawContainerSprite(parent: bottomSprite, scale: 0.5, color: .orange)
topSprite = drawContainerSprite(parent: middleSprite, scale: 1.0, color: .red)
setupComplete = true
}
let markerSprite = drawMarkerSprite(parent: self.scene!, scale: 1.0)
markerSprite.color = getMarkerColor(outerSprite: topSprite, innerSprite: markerSprite)
}
}

Here is what you're missing: SKNode.contains(_:) is not the same as CGRect.contains(_:).
SKNode.contains(_:) is not about rectangles. It's about hit-testing whether a point is inside a parent node. CGRect.contains(_:) is about rectangles, and it knows nothing about nodes, or scaling, or SpriteKit. If you're going to use a sprite's frame to check whether the gremlins are getting out of jail, you'll have to scale it yourself.

Related

How to create travelling wave in SpriteKit

How can I create a visual effect of traveling wave like this in Swift SpriteKit?
I am using an extension to the SKAction that performs the oscillatory movement in the node, but I still do not know how to create the trail. I though in creating copies but it did not worked.
extension SKAction {
static func oscillation(scene: SKScene, amplitude a: CGFloat, timePeriod t: CGFloat, midPoint: CGPoint) -> SKAction {
let action = SKAction.customAction(withDuration: Double(t)) { node, currentTime in
let displacement = a * sin(2 * .pi * currentTime / t)
node.position.y = midPoint.y + displacement
let copy = node.copy() as! SKSpriteNode
copy.position.x = node.position.x
copy.position.y = node.position.y
scene.addChild(copy)
}
return action
}
}
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let node = SKSpriteNode(color: UIColor.greenColor(), size: CGSize(width: 50, height: 50))
node.position = CGPoint(x: 25, y: size.height / 2)
self.addChild(node)
let oscillate = SKAction.oscillation(amplitude: 200, timePeriod: 1, midPoint: node.position)
node.runAction(SKAction.repeatActionForever(oscillate))
node.runAction(SKAction.moveByX(size.width, y: 0, duration: 5))
}
}
I would try a particle emitter attached to the moving node. It could be set to produce dot shaped particles quickly enough that they would overlap and make a line. The particles would move at a constant speed to the left, with no variations or fade. Set the count high enough that the oldest particles go out of view before disappearing. If you don’t need a solid line, you could cut down the emitter rate and make a dotted line showing the traveling wave. There are oodles of options in particle emitters, so you could play around with the settings to get other effects if appropriate for your project.

SpriteKit background image scrolling flickers at seam line

I am creating a 2D macOS game using SpriteKit and I want a continuous scrolling background from left to right.
I have a background image that is the same size as my frame. The image is duplicated side-by-side: initially the left one is centred and the right is off-screen. Both images (SKSpriteNode) are animated to slide across the screen and then reset their positions once moved by a full frame-width.
Here is the relevant code:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var background = SKSpriteNode()
func makeBackground() {
let texture = SKTexture(imageNamed: "bg-empty")
let scroll = SKAction.move(by: CGVector(dx: -texture.size().width, dy: 0), duration: 30)
let reset = SKAction.move(by: CGVector(dx: texture.size().width, dy: 0), duration: 0)
let animation = SKAction.repeatForever(SKAction.sequence([scroll, reset]))
for idx in 0...1 {
background = SKSpriteNode(texture: texture)
background.position = CGPoint(x: CGFloat(idx)*texture.size().width, y: self.frame.midY)
background.zPosition = -1
background.run(animation)
self.addChild(background)
}
}
override func didMove(to view: SKView) {
makeBackground()
}
}
Although this works, I notice a black (~1 pixel vertical strip) flicker that appears ad-hoc at the seam of the connection.
What is causing this flicker and how do I get rid of it?
You are encountering floating point rounding errors. This is going to lead to a situation where your first BG rounds down, and your second BG rounds up, giving you a 1 pixel gap.
instead, try the following code
class GameScene: SKScene {
private var background = SKNode()
func makeBackground() {
let texture = SKTexture(imageNamed: "bg-empty")
let scroll = SKAction.move(by: CGVector(dx: -texture.size().width, dy: 0), duration: 30)
let reset = SKAction.move(by: CGVector(dx: texture.size().width, dy: 0), duration: 0)
let animation = SKAction.repeatForever(SKAction.sequence([scroll, reset]))
for idx in 0...1 {
let subNode = SKSpriteNode(texture: texture)
subNode.position = CGPoint(x: CGFloat(idx)*texture.size().width, y: self.frame.midY)
background.addChild(subNode)
}
background.zPosition = -1
background.run(animation)
self.addChild(background)
}
override func didMove(to view: SKView) {
makeBackground()
}
}
Now you can avoid the gap because you are not running 2 different actions.
If you want to be able to scroll both ways. then place a texture to the left of your bg.
This will place a background node before and after your main background, and essentially turn it into 1 big node, with only the middle texture showing in its entirety.
If you have multiple background images, then just place the last frame of your background also to the left

Creating a background sprite node

I'm trying to create an endless scroller game and I'm trying to create two backgrounds.
I have a background class like so,
class BackgroundMG1: SKNode {
let background: SKSpriteNode
let size: CGSize
init(size: CGSize) {
self.size = size
let hue = CGFloat((arc4random() % 1000) / 1000)
let randomColor = UIColor(hue: hue, saturation: 1, brightness: 1, alpha: 1)
self.background = SKSpriteNode(color: randomColor, size: size)
super.init()
setup()
}
func setup() {
background.position.x = 0
background.position.y = 0
addChild(background)
}
}
I'm going to be populating my background with other objects so I needed it to be a class and not just a static image.
I have my game scene like so,
class AntarcticaMiniGame1: SKScene {
var player: SKSpriteNode!
var backgrounds: [BackgroundMG1]!
override func didMove(to view: SKView) {
setup()
}
func setup(){
// Initializers
player = Player()
// Positions
player.position.x = 0
player.position.y = 0
player.zPosition = 1
// Set up backgrounds
backgrounds = [BackgroundMG1]()
for i in 0..<2 {
let background = BackgroundMG1(size: size)
background.position.x = CGFloat(i) * size.width
background.position.y = 0
backgrounds.append(background)
addChild(background)
}
// Add to scene
addChild(player)
}
override func update(_ currentTime: TimeInterval) {
player.physicsBody!.applyForce(CGVector(dx: 25, dy:0))
}
}
You can also assume that I have a ground and camera object set up. I had to omit that code because it's not relevant to this question. The player stays on the ground and the camera follows the player.
Right now, when I load the game, the player object is moving to the right as expected. I see one background, but the second one doesn't show up.
I feel like I'm making some mistakes with the positioning. I'm not a 100% how the coordinate system works in Swift.
Edit: My scene is connected to a .sks file which is totally empty right now. The game is in landscape orientation.

How do I create a circular collision bound with Swift UIDynamics?

This post has been modified to include a screenshot and code...
It's been four days of trying to solve this, so can someone please clearly point out what's wrong with my implementation? It's my first time lol.
Have a look at my screenshot below.
-I'm using UIKit Dynamics and am trying to trap those 3 smaller balls inside the ROUND bounds of the larger ball, but they are only being kept inside the bounds of the larger ball's rectangle.
Here's the relative code:
The large bubble is created in IB and is declared in my class:
#IBOutlet weak var homeMainBubble: UIButton!
var gravity: UIGravityBehavior!
var animator: UIDynamicAnimator!
var collision: UICollisionBehavior!
var ballOne : UIImageView!
var ballTwo : UIImageView!
var ballThree : UIImageView!
var circlePath : UIBezierPath!
override func viewDidAppear(_ animated: Bool) {
circlePath = UIBezierPath(arcCenter: CGPoint(x: homeMainBubble.frame.midX,y: homeMainBubble.frame.midY), radius: homeMainBubble.frame.width/2, startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)}
In viewDidAppear(), I create a UIBezierPath()
//Adds or removes fake balls inside 'mainBubble()' when swiped up or down---------------------
func addOrRemoveFakeBalls(fakeBalls: String) {
let ballOneIs = positionOneBubble()
let ballImages = ["playBubble", "statsBubble", "settingsBubble", "cheatsBubble"]
let ballOneImage = ballImages[ballOneIs.tag - 1] //Determines the image to used for 'fake ball 1' based on the bubble in position one
var ballTwoImage : String = ""
var ballThreeImage : String = ""
if ballOneImage == "playBubble" { //Derivitive from ballOneImage, this determines the ball to the left & ensures correct array looping.
ballTwoImage = ballImages[3]
} else {ballTwoImage = ballImages[ballOneIs.tag - 2]}
if ballOneImage == "cheatsBubble" { //Derivitive from ballOneImage, this determines the ball to the right & ensures correct array looping.
ballThreeImage = ballImages[0]
} else {ballThreeImage = ballImages[ballOneIs.tag]}
switch fakeBalls {
case "Add":
ballOne = Ellipse(frame: CGRect(x: 125, y: 50, width: largeBubbleWidth, height: largeBubbleWidth))
ballOne.image = UIImage(named: ballOneImage)
ballOne.layer.zPosition = 1
ballOne.alpha = 0.3
ballTwo = Ellipse(frame: CGRect(x: 100, y: 50, width: mediumBubbleWidth, height: mediumBubbleWidth))
ballTwo.image = UIImage(named: ballTwoImage)
ballTwo.layer.zPosition = 1
ballTwo.alpha = 0.3
ballThree = Ellipse(frame: CGRect(x: 150, y: 50, width: mediumBubbleWidth, height: mediumBubbleWidth))
ballThree.image = UIImage(named: ballThreeImage)
ballThree.layer.zPosition = 1
ballThree.alpha = 0.3
homeMainBubble.addSubview(ballOne)
homeMainBubble.addSubview(ballTwo)
homeMainBubble.addSubview(ballThree)
animator = UIDynamicAnimator(referenceView: homeMainBubble)
gravity = UIGravityBehavior(items: [ballOne, ballTwo, ballThree])
gravity.magnitude = 1
animator.addBehavior(gravity)
collision = UICollisionBehavior(items: [ballOne, ballTwo, ballThree])
collision.addBoundary(withIdentifier: "Circle" as NSCopying, for: circlePath)
collision.collisionDelegate = self
collision.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)
case "Remove":
animator.removeAllBehaviors()
gravity.removeItem(ballOne)
gravity.removeItem(ballTwo)
gravity.removeItem(ballThree)
collision.removeAllBoundaries()
ballOne.removeFromSuperview()
ballTwo.removeFromSuperview()
ballThree.removeFromSuperview()
default: break
}
}
I've temporarily added collision.translatesReferenceBoundsIntoBoundary = true so that the balls don't drop off the screen, but with that commented out, why does the 'circlePath' UIBezierPath() not hold my smaller balls inside?
I have pretty much tried every approach and had every outcome EXCEPT for what I want :P.
What am I missing in order to keep the 3 smaller balls trapped inside the large ball's image (not rectangle) bounds?
THANK YOU for saving my week! :)
If you are using UIDynamics then you can follow the advice in this other question & answer
Namely, looks like you'll want to define collisionBoundingPath to be a UIBezierPath that is round.
override public var collisionBoundingPath: UIBezierPath {
let radius = min(bounds.size.width, bounds.size.height) / 2.0
return UIBezierPath(arcCenter: CGPointZero, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
}
From there you can customize the behavior of the smaller balls hitting the outer edge through the UICollisionBehaviorDelegate (again, see linked question and answer).
Update
With more code posted, another issue I think you are having is the UICollisionBehavior isn't including the "main ball". You are only adding collision behavior between the 3 sub-balls, which is likely why they are not properly interacting with the big ball as you'd want. Hopefully that helps!

SpriteKit partial texture mapping

Is it possible to create a SKSpriteNode that displays only a part of a texture?
For example, can I create a square with size 100x100 displaying the specific region of a texture of size 720x720 like from x1=300 to x2=400 and y1=600 to y2=700?
Thanks for your help.
Try something like this:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
let visibleArea = SKSpriteNode(color: .black, size: CGSize(width:100,height:100))
let parentNode = SKSpriteNode(color: .white, size: CGSize(width:200, height:200))
override func didMove(to view: SKView) {
let cropNode = SKCropNode()
let texture = SKSpriteNode(imageNamed: "Spaceship")
visibleArea.position = CGPoint(x: 0, y: 100)
cropNode.maskNode = visibleArea
cropNode.addChild(texture)
addChild(cropNode)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let previousPosition = touch.previousLocation(in: self)
let translation = CGPoint(x: location.x - previousPosition.x , y: location.y - previousPosition.y )
visibleArea.position = CGPoint(x: visibleArea.position.x + translation.x , y: visibleArea.position.y + translation.y)
}
}
}
Overriden touchesMoved method is there just because of better example. What I did here, is:
created SKCropNode
added a texture to it which will be masked
defined visible area which is SKSpriteNode and assigned it to crop node's mask property, which actually does the magic
Here is the result:
If you want to break up a texture into smaller chunks of textures to be used as puzzle pieces, then you want to use SKTexture(rect: in texture:)
Here is an example of how to use it:
let texture = SKTexture(...) //How ever you plan on getting main texture
let subTextureRect = CGRect(x:0,y:0.width:10,height:10) // The actual location and size of where you want to grab the sub texture from main texture
let subTexture = SKTexture(rect:subTextureRect, in:texture);
You now have a chunk of the sub texture to use in other nodes.