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
Related
I'm having a hard time setting boundaries and positioning camera properly inside my view after panning. So here's my scenario.
I have a node that is bigger than the screen and I want to let user pan around to see the full map. My node is 1000 by 1400 when the view is 640 by 1136. Sprites inside the map node have the default anchor point.
Then I've added a camera to the map node and set it's position to (0.5, 0.5).
Now I'm wondering if I should be changing the position of the camera or the map node when the user pans the screen ? The first approach seems to be problematic, since I can't simply add translation to the camera position because position is defined as (0.5, 0.5) and translation values are way bigger than that. So I tried multiplying/dividing it by the screen size but that doesn't seem to work. Is the second approach better ?
var map = Map(size: CGSize(width: 1000, height: 1400))
override func didMove(to view: SKView) {
(...)
let pan = UIPanGestureRecognizer(target: self, action: #selector(panned(sender:)))
view.addGestureRecognizer(pan)
self.anchorPoint = CGPoint.zero
self.cam = SKCameraNode()
self.cam.name = "camera"
self.camera = cam
self.addChild(map)
self.map.addChild(self.cam!)
cam.position = CGPoint(x: 0.5, y: 0.5)
}
var previousTranslateX:CGFloat = 0.0
func panned (sender:UIPanGestureRecognizer) {
let currentTranslateX = sender.translation(in: view!).x
//calculate translation since last measurement
let translateX = currentTranslateX - previousTranslateX
let xMargin = (map.nodeSize.width - self.frame.width)/2
var newCamPosition = CGPoint(x: cam.position.x, y: cam.position.y)
let newPositionX = cam.position.x*self.frame.width + translateX
// since the camera x is 320, our limits are 140 and 460 ?
if newPositionX > self.frame.width/2 - xMargin && newPositionX < self.frame.width - xMargin {
newCamPosition.x = newPositionX/self.frame.width
}
centerCameraOnPoint(point: newCamPosition)
//(re-)set previous measurement
if sender.state == .ended {
previousTranslateX = 0
} else {
previousTranslateX = currentTranslateX
}
}
func centerCameraOnPoint(point: CGPoint) {
if cam != nil {
cam.position = point
}
}
Your camera is actually at a pixel point 0.5 points to the right of the centre, and 0.5 points up from the centre. At (0, 0) your camera is dead centre of the screen.
I think the mistake you've made is a conceptual one, thinking that anchor point of the scene (0.5, 0.5) is the same as the centre coordinates of the scene.
If you're working in pixels, which it seems you are, then a camera position of (500, 700) will be at the top right of your map, ( -500, -700 ) will be at the bottom left.
This assumes you're using the midpoint anchor that comes default with the Xcode SpriteKit template.
Which means the answer to your question is: Literally move the camera as you please, around your map, since you'll now be confident in the knowledge it's pixel literal.
With one caveat...
a lot of games use constraints to stop the camera somewhat before it gets to the edge of a map so that the map isn't half off and half on the screen. In this way the map's edge is showing, but the furthest the camera travels is only enough to reveal that edge of the map. This becomes a constraints based effort when you have a player/character that can walk/move to the edge, but the camera doesn't go all the way out there.
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 am creating an app in Sprite Kit and Swift and want to create a sprite that jumps upwards while the screen is pressed, therefore jumping higher as the screen is pressed higher. I have achieved that effect, but gravity in the physics engine is not being applied until after the screen is released. Therefore the jumps are infinite (increasing exponentially) rather than levelling off at a certain point (like a quadratic equation / parabola).
How does one apply gravity actively during the motion of a sprite?
Here is my basic movement code:
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
// touched is true while screen is touched
if touched {
nodeLeft.physicsBody?.applyImpulse(CGVector(dx: -5, dy: 0))
}
}
NOTE: The object is jumping right and left rather than up and down
The gravity should be working constantly, and probably is. As you apply an impulse on every tick however, this force is much stronger than the gravity.
What you need to do is to decrease the effect of the impulse over time.
This can be achieved in a number of ways, for instance by using the position of the sprite as a base for the impulse: The higher the sprite position, the lower the impulse.
if touched {
let minX: CGFloat = 200 // some suitable value
let multiplier: CGFloat = 10
let force = max(0, (nodeLeft.position.x / minX - 1) * multiplier)
nodeLeft.physicsBody?.applyImpulse(CGVector(dx: -force, dy: 0))
}
The minX value in the above example probably makes no sense. But the logic is fairly sound I believe. You obviously need to experiment and tweak this (and the multiplier) to suit your needs.
I am building a ios game with swift and I have run into a bit of a problem. I am trying to spawn balls from the top of the screen and have them come down towards the ground. They are supposed to have random x values and go down at random rates but instead of spawning on the screen the nodes spawn on an x value which is not encompassed by the screen. Please help me as I think I have done everything right.
Here is the code for my addball function...
func addBall(){
//create ball sprite
var ball = SKSpriteNode(imageNamed: "ball.png")
//create physics for ball
ball.physicsBody = SKPhysicsBody(rectangleOfSize: ball.size) // 1
ball.physicsBody?.dynamic = true // 2
ball.physicsBody?.categoryBitMask = PhysicsCategory.Ball // 3
ball.physicsBody?.contactTestBitMask = PhysicsCategory.Person & PhysicsCategory.Ground
ball.physicsBody?.collisionBitMask = PhysicsCategory.None // 5
//generate random postion along x axis for ball to spawn
let actualX = random(min:ball.frame.size.width/2+1, max: self.frame.size.width - ball.frame.size.width/2-1)
println(actualX)
//set balls positon
ball.position = CGPoint(x: actualX, y: size.height - ball.size.width/2)
//add ball to scene
addChild(ball)
//determine speed of ball
let actualDuration = random(min: CGFloat(3.0), max: CGFloat(5.0))
//create movement actions
let actionMove = SKAction.moveTo(CGPoint(x:actualX, y: -ball.size.width/2), duration: NSTimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
ball.runAction(SKAction.sequence([actionMove, actionMoveDone]), withKey: "action")
}
here is the code for my random functions
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(#min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
The problem here is that your SKScene likely takes up much more space than the screen of your device. Thus, when you calculate a random value using the whole scene, some of the time the ball will spawn in the area of the scene not visible to you.
The two main properties that control the scene's size are its size and scaleMode properties. The scaleMode property relates to how the scene is mapped. Unless you initialized and presented this scene yourself, you can check the scaleMode in your view controller. It will likely be set to aspectFill, which according to Apple means:
The scaling factor of each dimension is calculated and the larger of the two is chosen. Each axis of the scene is scaled by the same scaling factor. This guarantees that the entire area of the view is filled but may cause parts of the scene to be cropped.
If you don't like this, there are other scaleModes. However, in most cases this mode would actually be preferable since SpriteKit's internal scaling is able to make universal apps. If this is fine for you, then the easiest thing to do is set hardcoded values for something like the spawn locations for your ball node.
I'm looking for the proper SpriteKit way to handle something of a scrollable world. Consider the following image:
In this contrived example, the world boundary is the dashed line and the blue dot can move anywhere within these boundaries. However, at any given point, a portion of this world can exist off-screen as indicated by the image. I would like to know how I can move the blue dot anywhere around the "world" while keeping the camera stationary on the blue dot.
This is Adventure, a sprite kit game by apple to demonstrate the point I made below. Read through the docs, they explain everything
Theres a good answer to this that I can't find at the moment. The basic idea is this:
Add a 'world' node to your scene. You can give it a width/height that is larger than the screen size.
When you 'move' the character around (or blue dot), you actually move your world node instead, but in the opposite direction, and that gives the impression that you're moving.
This way the screen is always centered on the blue dot, yet the world around you moves
below is an example from when I was experimenting a while ago:
override func didMoveToView(view: SKView) {
self.anchorPoint = CGPointMake(0.5, 0.5)
//self.size = CGSizeMake(600, 600)
// Add world
world = SKShapeNode(rectOfSize: CGSize(width: 500, height: 500))
world.fillColor = SKColor.whiteColor()
world.position = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
world.physicsBody?.usesPreciseCollisionDetection = true
self.addChild(world)
}
override func update(currentTime: CFTimeInterval) {
world.position.x = -player.position.x
world.position.y = -player.position.y
}
override func didSimulatePhysics() {
self.centerOnNode(self.camera)
}
func centerOnNode(node: SKNode) {
if let parent = node.parent {
let nodePositionInScene: CGPoint = node.scene!.convertPoint(node.position, fromNode: parent)
parent.position = CGPoint(
x: parent.position.x - nodePositionInScene.x,
y: parent.position.y - nodePositionInScene.y)
}}
If you create a "camera" node which you add to your "world" node, a couple of simple functions (above) allow you to "follow" this camera node as it travels through the world, though actually you are moving the world around similar to Abdul Ahmad's answer.
This method allows you to use SpriteKit functionality on the camera. You can apply physics to it, run actions on it, put constraints on it, allowing effects like:
camera shaking (an action),
collision (a physics body, or matching the position of another node with a physics body),
a lagging follow (place a constraint on the camera that keeps it a certain distance from a character, for example)
The constraint especially adds a nice touch to a moving world as it allows the "main character" to move around freely somewhat while only moving the world when close to the edges of the screen.