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.
Related
I created a Sprite SKNode Class which works like a StackView. In Sprite Kit objects origin is always centered therefore I would like to offset the position by a certain amount (which is calculateAccumulatedFrame().width/2) so it can be positioned comfortable. What would be the best way to do so?
First I thought I could do something like this
override var position: CGPoint {
didSet {
self.position = CGPoint(x: position.x - self.calculateAccumulatedFrame().width/2, y: position.y)
}
}
But of course this is not possible since this would end in an endless loop
What would be other options?
In Sprite Kit objects origin is always centered
That is not true. There is this useful property called anchorPoint that you can set to specify where the "origin" is.
Judging from your code, you seem to want the anchor point to be on the midpoint of the right edge of the sprite, assuming the sprite to be a rectangle. This means that you want the anchor point to be (1, 0.5):
yourSprite.anchorPoint = CGPoint(x: 1, y: 0.5)
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.
I have an infinitely scrolling background and a character 'walking' on the ground plane.
I want to drop objects from above and have them land on the ground - so far so good. But the objects don't move with the ground.
Can't find any examples where this is taken care of.
my 'ground' is just an edge at the correct height with regard to my graphics background.
let Edge = SKNode()
Edge.physicsBody = SKPhysicsBody(edgeFromPoint: CGPointZero, toPoint: CGPointMake(self.frame.width + 1, 0))
Edge.position = CGPointMake(0, bottomShelf_ItemStartPosition.y)
self.addChild(Edge)
I've tried giving my ground SKSriteNode physical properties, but this didn't make any difference.
What would be the best approach for this? Must be a simple way to make the moving ground effect objects.
adding scroll routine:
func backgroudScrollUpdate1(){
back.position = CGPoint(x: back.position.x - scrollPerFrameAmount, y: back.position.y)
back2.position = CGPoint(x: back2.position.x - scrollPerFrameAmount, y: back2.position.y)
if (back.position.x < -(back.size.width / 2)) {
back.position = CGPointMake(back2.position.x + back.size.width, back.position.y)
}
if (back2.position.x < -(back.size.width / 2)) {
back2.position = CGPointMake(back.position.x + back2.size.width, back2.position.y)
}
}
update - some progress
i have set up a physics world, where my ground is infinitely scrolling. I've got some items that fall out of the 'sky' and land on the ground. I can move the things around by applying impulse directly or one thing hitting another.
But, what i expected is that the scrolling ground would 'pull' things along with it if they are on the ground?
I have played around with different friction levels of both the ground at the items touching the ground, but it doesn't seem to matter.
one thing that did sort of work is if i set the phsyics body of an item to a circle, then the ground does influence the item, turning it around in the opposite direction of the scrolling - but when it hits an edge it just stops, rather than spinning at the edge.
If that part is working, then why wouldn't a rectangle be dragged along by the ground ?
would love to see example code of a phsyics world that does have the ground effecting other nodes..
I am working with SpriteKit right now and I came across a problem that seems simple but I couldn't find anything on the internet. I have three buttons that are shaped like parallelograms stacked on top of each other and it looks like this:
Screenshot of buttons
let button = SKSpriteNode(imageNamed: "playbutton")
let leaderButton = SKSpriteNode(imageNamed: "leaderbutton")
let homeButton = SKSpriteNode(imageNamed: "homebutton")
button.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(button)
leaderButton.position = CGPoint(x: size.width/2, y: size.height/2 - button.size.height/2)
addChild(leaderButton)
homeButton.position = CGPoint(x: size.width/2, y: size.height/2 - button.size.height)
addChild(homeButton)
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if button.containsPoint(location) {
button.runAction(SKAction.scaleTo(0.8, duration: 0.1))
}
else if leaderButton.containsPoint(location) {
leaderButton.runAction(SKAction.scaleTo(0.8, duration: 0.1))
}
else if homeButton.containsPoint(location) {
homeButton.runAction(SKAction.scaleTo(0.8, duration: 0.1))
}
}
}
This is how I am detecting touches. The problem is that they overlap because the sprite is actually a rectangle so when i try and tap on the top left of the second button, the top button detects it. I was wondering of there is a way to detect touch only in the texture like how you can set the physics body to a texture.
Thanks for any help you can give me!
Link works now.
So I tried this:
button.position = CGPoint(x: size.width/2, y: size.height/2)
button.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "playbutton"), size: button.size)
button.physicsBody?.dynamic = false
addChild(button)
leaderButton.position = CGPoint(x: size.width/2, y: size.height/2 - button.size.height/2)
leaderButton.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "leaderbutton"), size: leaderButton.size)
leaderButton.physicsBody?.dynamic = false
addChild(leaderButton)
homeButton.position = CGPoint(x: size.width/2, y: size.height/2 - button.size.height)
homeButton.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "homebutton"), size: homeButton.size)
homeButton.physicsBody?.dynamic = false
addChild(homeButton)
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if physicsWorld.bodyAtPoint(location)?.node == button {
button.runAction(SKAction.scaleTo(0.8, duration: 0.1))
print("play")
}
if physicsWorld.bodyAtPoint(location)?.node == leaderButton {
leaderButton.runAction(SKAction.scaleTo(0.8, duration: 0.1))
print("leader")
}
if physicsWorld.bodyAtPoint(location)?.node == homeButton {
homeButton.runAction(SKAction.scaleTo(0.8, duration: 0.1))
}
}
}
It still registers the full frame and not just the physics body. See the link to see the buttons and how their coordinates intersect.
If you attach a physics body to each button, you can detect which physics body your touch lands on.
You can generate a physics body from the button's texture (assuming the button is an SKSpriteNode) using SKPhysicsBody(texture:size:) or SKPhysicsBody(texture:alphaThreshold:size:), or you can create a CGPath describing the button's shape and use SKPhysicsBody(polygonFromPath:). Assign the body to the button's physicsBody property. Assuming you don't actually want the physics simulator to move your buttons, set each body's dynamic property to false.
Then you can use physicsWorld.bodyAtPoint(_:) to get one of the bodies that a touch lands on (where physicsWorld is a property of your SKScene). Use the body's node property to get back to the button node. If bodies overlap, bodyAtPoint returns an arbitrary body.
You can use physicsWorld.enumerateBodiesAtPoint(_:usingBlock:) if you need all of the bodies that your touch lands on.
A completely different approach, if you can create a CGPath describing the button's shape, is to use SKScene.convertPointFromView(_:) and then SKNode.convertPoint(_:fromNode:_) to convert the point into the button's coordinate system, and then use CGPathContainsPoint (a global function) to detect whether the point is in the path describing the button shape.
(I can't see your imgur link, but I think I have a pretty good idea of what you're describing.)
There are some discussions on how to read the alpha value of out an SKTexture, but those require quite a bit of overhead for just "did I touch a parallelogram"? Especially if you add alpha effects to your buttons.
You can do whatever logic you want on the location - you have a CGPoint that contains the location, and you know the size and shape of your buttons.
It should be pretty straightforward to write a function to test whether a point is inside a parallelogram-shaped button. Since parallelograms are basically rectangles with triangles on each end of an "inner" rect, if you know the size of that "inner" rect, you can pretty easily determine whether the touch is where you want by:
Checking that it's in the rect of the entire button. If not, you know it's not in the parallelogram.
Checking to see if it's inside the "inner" rectangle - the part of the parallelogram with the triangles "chopped off" the ends. If so, then you know it's in the parallelogram.
Checking to see if it's inside one of the triangles. You know how far up and how far across the touch is in the rect that contains the triangle, and you know the slope of the line that divides that rect to make the triangle by virtue of its width and height. That makes it trivial to check whether the point is in the triangle or not - just use the "y=mx+b" formula for a line, where m is the slope and b is whatever you need to add to get the line to pass through the corner of the rect (typically your "y intercept"). If the y coordinate is less than/greater than m*(the x coordinate) + b, you can determine whether the touch is inside or outside that triangle.
Once you have that function, you can use it as the test for checking each button.
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