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.
Related
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")
}
}
I have a weird problem where my SKReferenceNode does not collide properly after being scaled up. It collides well in the center, but it ignores collisions and contacts on the edges.
Here is the first photo of the scene. The SKReferenceNode was scaled up significantly, and as seen in this photo, does not collide correctly on the edges. The PhysicsBody appears to be correct (with showPhysics on), yet the ball refuses to collide.
The SKReferenceNode is using an alpha collision mask, because I need to change it to a larger sprite in the future to do animations and such. Additionally, non-scaled objects work completely fine. Finally, after the ball collides with the center, which does work, and the block is reset, collisions start working as expected again. The code to fix it is probably in the reset function, but I reset everything before the level is loaded, so this wouldn't make sense. Here is a part of my reset code:
func reset() {
physicsWorld.gravity = CGVectorMake(0, -9.8)
for grav in gravityBlock {
grav.reset()
}
gravity = -9.8
//Resets blocks
for blocks in destroyedBlocks { //the blocks are stored in destroyedBlocks when collided with
blocks.reset()
}
destroyedBlocks = []
/*Irrelevant code removed*/
}
Here's my blocks.reset() function:
override func reset() {
super.reset()
self.physicsBody?.categoryBitMask = 1
removeAllActions()
self.texture = self.text
shadow.hidden = false
self.alpha = 0
shadow.alpha = 0
let appear = SKAction(named: "Appear")!
self.runAction(appear)
shadow.runAction(SKAction.fadeInWithDuration(0.25))
}
Here is super.reset()
func reset() {
self.hidden = false
state = .NotBroken
}
Thanks so much!
When you scale the sprite you are just changing it's visual representation. The Physics Body stays the same. That's why you are getting the "strange" during the collisions.
You can see that showing the physics uses in your scene, just open GameViewController and add this line
skView.showsPhysics = true
inside viewDidLoad, just after this line
skView.showsNodeCount = true
Then run again your game, the physics boundaries now will be highlighted.
I'm using SpriteKit for a game where there is a ball and a character. The character is moving in a line with SKAction and the user can move the ball by touch. When the ball and character makes contact, I'd like the ball to be attached to the character.
func didBeginContact(contact: SKPhysicsContact) {
if contact.bodyA.categoryBitMask == charMask && contact.bodyB.categoryBitMask == ballMask{
println("contact")
if contactMade {
let movingBall = contact.bodyB.node!.copy() as! SKSpriteNode
//this line of code doesn't affect anything no matter what coordinates i set it to
movingBall.position = contact.bodyA.node!.position
contact.bodyB.node!.removeFromParent()
contact.bodyA.node!.removeAllActions()
contact.bodyA.node!.addChild(movingBall)
}
}
}
The current issue is that when I do contact.bodyA.node!.addChild(ball) (character node), the ball gets attached to the far left side of the character instead of on the character and it seems setting movingball.position does not affect it. I've also tried doing addChild to the scene and although it adds the ball to the correct place, it doesn't move with the character without another SKAction. What did I do wrong/what can I do to fix it?
Also, instead of using 2 sprites like that, should I use separate sprites so that theres a character sprite without a ball, character sprite with a ball and the ball itself, then just change the node's texture when they contact?
as movingBall is to be a child of bodyA.node, it's position needs to be in bodyA.node's coordinate space. So if you gave it coordinates of (0,0) it'd be in the middle:
Replace:
movingBall.position = contact.bodyA.node!.position
with
movingBall.position = CGPointZero
and see how that looks. the ball should be positioned directly on top of bodyA.node.
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
I have a function in SpriteKit that spawns a sprite (which is a square) at the top of the screen, and then gravity pulls the sprite to the bottom of the screen. I'm trying to get the sprite to rotate smoothly for an indefinite amount of time until it is removed when it reaches the bottom of the screen. The following code is in the class for the sprite:
func rotate() {
var action = SKAction.rotateByAngle(CGFloat(M_PI_2), duration: NSTimeInterval(1.5))
var repeatAction = SKAction.repeatActionForever(action)
self.runAction(repeatAction)
}
The problem that I am having is that, as the sprite turns, the sprite travels in the direction of the bottom of itself, not towards the bottom of the screen (the direction gravity is supposed to be). To further clarify, the object rotates, but as it rotates to 90 degrees, it travels sideways instead of downwards. This doesn't make sense to me. This is the code I'm using to add gravity in the didMoveToView() function:
self.physicsWorld.gravity = CGVectorMake(0.0, -1.8)
and this is the code used to spawn the sprite (the rs.rotate() calls the rotate method that is listed above):
func spawnRedSquares() {
if !self.gameOver {
let rs = RedSquare()
var rsSpawnRange = randomNumberBetween(self.leftSideBar.position.x + rs.sprite.size.width / 2, secondNum: self.rightSideBar.position.x - rs.sprite.size.width / 2)
rs.position = CGPointMake(rsSpawnRange, CGRectGetMaxY(self.frame) + rs.sprite.size.height * 2)
rs.zPosition = 3
self.addChild(rs)
self.rsArray.append(rs)
rs.rotate()
let spawn = SKAction.runBlock(self.spawnRedSquares)
let delay = SKAction.waitForDuration(NSTimeInterval(timeBetweenRedSquares))
let spawnThenDelay = SKAction.sequence([delay, spawn])
self.runAction(spawnThenDelay)
}
}
How can I get the object to rotate, but still fall down as if it were affected by normal gravity?
It looks like you are adding the rotation action to 'self' which I would assume is your scene as opposed to your sprite. This is causing the entire scene to rotate, and therefore its gravity is rotating as well.
Add the rotating action to your sprite and that should solve the issue.
Example: assuming your square sprite is called squareSprite:
let action = SKAction.rotateByAngle(CGFloat(M_PI_2), duration: NSTimeInterval(2))
let repeatAction = SKAction.repeatActionForever(action)
squareSprite.runAction(repeatAction) //Add the repeatAction to your square sprite