Swift SKSpriteNode Position Issues - logging correct, placement wrong - ios

the situation
I am working through a Lynda.com video series (iOS Game Dev. With Sprite Kit), the tutorial is last gen for Objective-C and as a training exercise I am converting it to Swift and playing with new functionality. The goal is an old school brick breaking game.
I have an updated full project repository on GitHub here
the issue
in the GameScene.swift file, I have written a method addBricks(size: CGSize) :
func addBricks (size: CGSize){
println(" the input parameter size: \(size)")
var maxRows = 1
var maxCols = 1
var xPos : CGFloat
var yPos : CGFloat
for (var rows = 0; rows < maxRows; rows++){
for (var i = 0; i < maxCols ; i++){
var brick: SKSpriteNode = SKSpriteNode(imageNamed: "brick")
brick.physicsBody = SKPhysicsBody(rectangleOfSize: brick.frame.size)
xPos = CGFloat(size.width) / CGFloat(maxCols+1) * CGFloat(i + 1)
yPos = CGFloat(size.height) - CGFloat(80 * rows) - 100.0
brick.physicsBody.dynamic = false
brick.physicsBody.categoryBitMask = brickCategory
brick.shadowCastBitMask = 1
brick.lightingBitMask = 1
//brick.position = CGPointMake(CGFloat(xPos), CGFloat(yPos))
brick.position = CGPointMake(320.0, 1036.0)
println("Brick position - xpos: \(xPos), ypos: \(yPos) || overall:\(brick.position)")
self.addChild(brick)
}
}
}
I first call it in the scene from didMoveToView() and everything is peachy, my brick nodes are placed in the appropriate locations. In my game logic I later call this method from didBeginContact(), when the ball strikes the paddle after a logic check to see there are no bricks left. In the "When you run out of bricks, add more" sense...
func didBeginContact(contact: SKPhysicsContact!){
var notTheBall : SKPhysicsBody
// check the contacts and find the one thats not the ball
if ( contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask ){
notTheBall = contact.bodyB
} else {
notTheBall = contact.bodyA
}
/* more logic here for other contacts */
if ( notTheBall.categoryBitMask == paddleCategory ){
self.runAction(playSFXBlip)
//println("\(self.children.count) and \(staticSize)")
if ( self.children.count <= 5 ){
addBricks(self.frame.size)
}
}
}
On any subsequent method calls to addBricks(), ALL of the bricks are placed in what appears to be point 0,0 at the bottom left of the view, even when console is showing they are placed at a different position:
the self object is the current instance of SKScene println(self) -> <SKScene> name:'(null)' frame:{{0, 0}, {640, 1136}}
i've tried:
casting all elements of my position calculation as Floats and CGFloats
debugging the addBricks method by setting static float values
making a property for frame size outside of the contact method and passing that size to addBricks.
google-ing and consulting the Swift Programming Language Guide and the docs for anything relevant
what's causing inappropriate positioning? thanks in advance sorry for the TL;DR

The problem is, that you're trying to set the position of a Node in your didBeginContact method. You cannot position a node in didBeginContact that already has a physicsbody.
The ideal solution would be to add the bricks without a physicsbody, then set it's position to where ever you like, and finally add the physicsbody.
Another solution is to create some code that is being called after you've run your didBeginContact. For example
func didBeginContact(contact: SKPhysicsContact!){
var notTheBall : SKPhysicsBody
if ( contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask ){
notTheBall = contact.bodyB
} else {
notTheBall = contact.bodyA
}
if ( notTheBall.categoryBitMask == paddleCategory ){
addBricksInUpdate = true
}
}
func update:(currentTime: CFTimeInterval) {
if (addBricksInUpdate) {
if ( self.children.count <= 5 ){
addBricks(self.frame.size)
}
}
}

I don't know enough about what "self" is when you are calling, or what self's properties and values are, but when child nodes appear in the bottom left corner it is usually for one of two reasons:
1-The objects have been placed at (0,0) - either because that's their position, or because some other constraint, like size or rect, returned zero values. So you have a parent with size (0,0), or converted coordinates, etc.
[OR]
2- The parent node to the objects is offset/scaled/positioned from the "screen" rect in such a way that the values you are inputting for position on the child have had the children end up looking like they are in the bottom left corner, even though they are actually being placed relative to the parent or the paren't coordinate space. This is easily checked by seeing if different coords still put the object in the same place.
So, check the anchor points for your parent, be it a scene or a node. And check the size and position of the parent. I've heard mention that there are strange default size settings for the sks file in swift with regards to sprite kit and scene size, but haven't opened a swift project yet.

Related

SpriteKit: how to smoothly animate SKCameraNode while tracking node but only after node moves Y pixels?

This question and others discuss how to track a node in SpriteKit using a SKCameraNode.
However, our needs vary.
Other solutions, such as updating the camera's position in update(_ currentTime: CFTimeInterval) of the SKScene, do not work because we only want to adjust the camera position after the node has moved Y pixels down the screen.
In other words, if the node moves 10 pixels up, the camera should remain still. If the node moves left or right, the camera should remain still.
We tried animating the camera's position over time instead of instantly, but running a SKAction against the camera inside of update(_ currentTime: CFTimeInterval) fails to do anything.
I just quickly made this. I believe this is what you are looking for?
(the actual animation is smooth, just i had to compress the GIF)
This is update Code:
-(void)update:(CFTimeInterval)currentTime {
/* Called before each frame is rendered */
SKShapeNode *ball = (SKShapeNode*)[self childNodeWithName:#"ball"];
if (ball.position.y>100) camera.position = ball.position;
if (fabs(ball.position.x-newLoc.x)>10) {
// move x
ball.position = CGPointMake(ball.position.x+stepX, ball.position.y);
}
if (fabs(ball.position.y-newLoc.y)>10) {
// move y
ball.position = CGPointMake(ball.position.x, ball.position.y+stepY);
}
}
I would not put this in the update code, try to keep your update section clutter free, remember you only have 16ms to work with.
Instead create a sub class for your character node, and override the position property. What we are basically saying is if your camera is 10 pixels away from your character, move towards your character. We use a key on our action so that we do not get multiple actions stacking up and a timing mode to allow for the camera to smoothly move to your point, instead of being instant.
class MyCharacter : SKSpriteNode
{
override var position : CGPoint
{
didSet
{
if let scene = self.scene, let camera = scene.camera,(abs(position.y - camera.position.y) > 10)
{
let move = SKAction.move(to: position, duration:0.1)
move.timingMode = .easeInEaseOut
camera.run(move,withKey:"moving")
}
}
}
}
Edit: #Epsilon reminded me that SKActions and SKPhysics access the variable directly instead of going through the stored property, so this will not work. In this case, do it at the didFinishUpdate method:
override func didFinishUpdate()
{
//character should be a known property to the class, calling find everytime is too slow
if let character = self.character, let camera = self.camera,(abs(character.position.y - camera.position.y) > 10)
{
let move = SKAction.move(to: character.position, duration:0.1)
move.timingMode = .easeInEaseOut
camera.run(move,withKey:"moving")
}
}

Detecting when SKSpriteNode is completely below another SKSpriteNode

For a game I'm creating, an SKSpriteNode gradually makes its way down the user's screen. There's another SKSpriteNode (position is static) near the bottom of the screen, leaving only a little bit of space for the original SKSpriteNode to fit in. I need to detect when the first SKSpriteNode is COMPLETELY below the second SKSpriteNode, which I'm having a bit of trouble with. Here's the code I'm currently using:
if (pos.y > barPosY) //pos.y = 1st SKSpriteNode, barPosY = 2nd SKSpriteNode
{
touchedTooEarly = true
}
For some reason, when the first SKSpriteNode goes just a little bit over the 2nd SKSpriteNode (not all the way), it still detects it as being completely over. Is there a coordinate space issue I'm missing?
The logic
A sprite a covers a sprite b if
b.frame is inside a.frame
b.zPosition is below a.zPosition
The extension
Now let's build an extension
extension SKSpriteNode {
func isCoveredBy(otherSprite: SKSpriteNode) -> Bool {
let otherFrame = CGRect(
origin: convertPoint(otherSprite.position, fromNode: otherSprite),
size: otherSprite.frame.size
)
return zPosition < otherSprite.zPosition && CGRectContainsRect(frame, otherFrame)
}
}
Problem #1: transparency
This mechanism totally ignores transparency.
Problem #2: same sprite
If you compare 2 sprites of the same type the function CGRectContainsRect will return true only when they are exactly aligned. Althoug you can solve this problem creating a smaller rectangle when you compare the sprites.

Centering Camera on Velocity Applied Node Not Working

I have a subclass of SKNode which consists of a few sprites that make up a player. I would like the "camera" to center on this node (Always have the player in the center). Now before you down vote this for it being a duplicate, hear me out. The Apple documents suggest making the player node completely static, and instead moving around a camera node. However in my case I'm applying multiple properties of physics to my character, including velocity impulses. My first thought would be to just apply these impulses to the camera node itself, however this has become impossible due to the fact that the character has a small soft-body physics engine on it. I'm applying velocity to it like so:
player.primaryCircle.physicsBody!.velocity = CGVector(dx: player.primaryCircle.physicsBody!.velocity.dx+relVel.dx*rate, dy: player.primaryCircle.physicsBody!.velocity.dy+relVel.dy*rate)
I managed to get it to partially work with the following code:
override func didSimulatePhysics() {
self.player.position = player.primaryCircle.position
self.camera.position = player.position
centerOnNode(camera)
}
func centerOnNode(node: SKNode) {
let cameraPositionInScene: CGPoint = node.scene!.convertPoint(node.position, fromNode: node.parent!)
node.parent!.position = CGPoint(x:node.parent!.position.x - cameraPositionInScene.x, y:node.parent!.position.y - cameraPositionInScene.y)
}
However that didn't 100% work, as seen here: (It should be focused on the red circle)
http://gyazo.com/b78950e6cc15b60f390cd8bfd407ab56
As you can see, the world/map is moving, however it doesn't seem to be moving fast enough to center the player in the middle. (And note that the "Unamed" text is at a fixed spot on the screen -- That's why it seems to always be in the center)
I think this should still work with physics unless I am not truly understanding the question. We did something similar with our SKATiledMap with that Auto Follow Feature. What you need to do is make sure the player is added to a node you can move (usually a map) as a child and then in the update function you do something like this...(sorry it isn't in swift)
-(void)update
{
if (self.autoFollowNode)
{
self.position = CGPointMake(-self.autoFollowNode.position.x+self.scene.size.width/2, -self.autoFollowNode.position.y+self.scene.size.height/2);
//keep map from going off screen
CGPoint position = self.position;
if (position.x > 0)
position.x = 0;
if (position.y > 0)
position.y = 0;
if (position.y < -self.mapHeight*self.tileWidth+self.scene.size.height)
position.y = -self.mapHeight*self.tileWidth+self.scene.size.height;
if (position.x < -self.mapWidth*self.tileWidth+self.scene.size.width)
position.x = -self.mapWidth*self.tileWidth+self.scene.size.width;
self.position = CGPointMake((int)(position.x), (int)(position.y));
}
}
Map being the node that the player is added to. Hopefully that helps. Also here is the link to the git hub project we have been working on. https://github.com/SpriteKitAlliance/SKAToolKit

Check if node is visible on the screen

I currently have a large map that goes off the screen, because of this its coordinate system is very different from my other nodes. This has led me to a problem, because I'm needing to generate a random CGPoint within the bounds of this map, and then if that point is frame/on-screen I place a visible node there. However the check on wether or not the node is on screen continuously fails.
I'm checking if the node is in frame with the following code: CGRectContainsPoint(self.frame, values) (With values being the random CGPoint I generated). Now this is where my problem comes in, the coordinate system of the frame is completely different from the coordinate system of the map.
For example, in the picture below the ball with the arrows pointing to it is at coordinates (479, 402) in the scene's coordinates, but they are actually at (9691, 9753) in the map's coordinates.
I determined the coordinates using the touchesBegan event for those who are wondering. So basically, how do I convert that map coordinate system to one that will work for the frame?
Because as seen below, the dot is obviously in the frame however the CGRectContainsPoint always fails. I've tried doing scene.convertPoint(position, fromNode: map) but it didn't work.
Edit: (to clarify some things)
My view hierarchy looks something like this:
The map node goes off screen and is about 10,000x10,000 for size. (I have it as a scrolling type map). The origin (Or 0,0) for this node is in the bottom left corner, where the map starts, meaning the origin is offscreen. In the picture above, I'm near the top right part of the map. I'm generating a random CGPoint with the following code (Passing it the maps frame) as an extension to CGPoint:
static func randPoint(within: CGRect) -> CGPoint {
var point = within.origin
point.x += CGFloat(arc4random() % UInt32(within.size.width))
point.y += CGFloat(arc4random() % UInt32(within.size.height))
return point;
}
I then have the following code (Called in didMoveToView, note that I'm applying this to nodes I'm generating - I just left that code out). Where values is the random position.
let values = CGPoint.randPoint(map.totalFrame)
if !CGRectContainsPoint(self.frame, convertPointToView(scene!.convertPoint(values, fromNode: map))) {
color = UIColor.clearColor()
}
To make nodes that are off screen be invisible. (Since the user can scroll the map background). This always passes as true, making all nodes invisible, even though nodes are indeed within the frame (As seen in the picture above, where I commented out the clear color code).
If I understand your question correctly, you have an SKScene that contains an SKSpriteNode that is larger than the scene's view, and that you are randomly generating coordinates within that sprite's coordinate system that you want to map to the view.
You're on the right track with SKNode's convertPoint(_:fromNode:) (where your scene is the SKNode and your map is the fromNode). That should get you from the generated map coordinate to the scene coordinate. Next, convert that coordinate to the view's coordinate system using your scene's convertPointToView(_:). The point will be out of bounds if it is not in view.
Using a worldNode which includes a playerNode and having the camera center on this node, you can check on/off with this code:
float left = player.position.x - 700;
float right = player.position.x + 700;
float up = player.position.y + 450;
float down = player.position.y - 450;
if((object.position.x > left) && (object.position.x < right) && (object.position.y > down) && (object.position.y < up)) {
if((object.parent == nil) && (object.dead == false)) {
[worldNode addChild:object];
}
} else {
if(object.parent != nil) {
[object removeFromParent];
}
}
The numbers I used above are static. You can also make them dynamic:
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat screenWidth = screenRect.size.width;
CGFloat screenHeight = screenRect.size.height;
Diving the screenWidth by 2 for left and right. Same for screenHeight.

Sprite Kit Collision without bouncing

I setup a hero and some platforms that are moving from the top downwards.
With these I have collisionBitMasks that detect when the hero lands on a platform if the hero comes from above (to let the hero jump through the platforms)
if (_hero.physicsBody.velocity.dy > 0) {
_hero.physicsBody.collisionBitMask = 0;
}
else {_hero.physicsBody.collisionBitMask = platformCategory;
}
Everything works fine, except that the hero keeps bouncing on the platform.
Is there a way to let him sit on it, while the platform is moving down?
I tried using physicsBody.resting and physicsBody.friction, but without any success.
Thanks for help Guys
Had the same issue just a minute ago. It works with setting the restitution but you need to set both restitutions. The one of the hero AND the one of the platform (or in my case the scene boundaries).
so:
hero.physicsBody.collisionBitMask = platformCategory
hero.physicsBody.restitution = 0.0
platform.physicsBody.restitution = 0.0
any_other_object_that_should_still_bounce.physicsBody.restitution = 1.0
will do it. All other objects on the screen still bounce on the platform as long as you set their restitution > 0
The trick is to avoid any physics behavior while the hero is on the platform by resetting the hero body's velocity and setting the hero's position to a fixed vertical offset from the platform's position.
In semi-pseudo-code:
-(void) didSimulatePhysics
{
if (<hero on platform>)
{
hero.physicsBody.velocity = CGPointZero;
hero.position = CGPointMake(hero.position.x,
platform.position.y + <offset as needed>);
}
}
Have you tried altering the restitution property of the nodes' physics bodies?
_hero.physicsbody.restitution = 0;
It controls the bounciness...
You can use the Collision category:
When the velocity.dy is < 0 the hero is falling.
func collisionWithPlayer(player: SKNode) {
// 1
if player.physicsBody?.velocity.dy < 0 && (player.position.y - player.frame.size.height / 2.0) >= (self.position.y){
// 2
player.physicsBody?.collisionBitMask = CollisionCategoryBitMask.Platform
}

Resources