using skphysicsBody and circle of radius to trap ball in circle - ios

roundbg.physicsBody = SKPhysicsBody(circleOfRadius: round.frame.width/2)
roundbg.physicsBody?.dynamic = false
Trying to trap the ball inside the circle(roundbg). The problem is that when I use circleOfRadius for my round texture it takes the whole circle and not just the border. I get why circleOfRadius is doing that but can't figure out how to grab just the border. Any ideas? So ideally when ball falls it gets trapped inside the circle(roundbg).
First iOS app/game and very new to this. Thanks for you help!

SKPhysicsBody(circleOfRadius:) creates a volume based body. In your case you need an Edge based body for the larger circle. Try the following code.
let largeCircleRadius : CGFloat = 100
var largeCirclePath = CGPathCreateMutable();
CGPathAddArc(largeCirclePath, nil, 0.0, 0.0, largeCircleRadius, 0.0, CGFloat(2.0 * M_PI), true)
let largeCircle = SKShapeNode(circleOfRadius: 100)
largeCircle.position = CGPointMake(CGRectGetWidth(frame) / 2.0, CGRectGetHeight(frame) / 2.0)
largeCircle.physicsBody = SKPhysicsBody(edgeLoopFromPath: largeCirclePath)
addChild(largeCircle)
let smallCircle = SKShapeNode(circleOfRadius: 10)
smallCircle.position = CGPointMake(CGRectGetWidth(frame) / 2.0, CGRectGetHeight(frame) / 2.0)
smallCircle.physicsBody = SKPhysicsBody(circleOfRadius: 10)
addChild(smallCircle)
Read more about Edge based and Volume based body here : https://developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Physics/Physics.html#//apple_ref/doc/uid/TP40013043-CH6-SW3
To create a fraction of a circle, you can change the startAngle and endAngle of the circlePath accordingly. Also use edgeChainFromPath instead of edgeLoopFromPath to not close the loop.
let largeCircleRadius : CGFloat = 100
var largeCirclePath = CGPathCreateMutable();
let startAngle : CGFloat = 3.14
let endAngle : CGFloat = 2 * 3.14
CGPathAddArc(largeCirclePath, nil, 0.0, 0.0, largeCircleRadius, startAngle, endAngle , false)
let largeCircle = SKShapeNode(path: largeCirclePath)
largeCircle.position = CGPointMake(CGRectGetWidth(frame) / 2.0, CGRectGetHeight(frame) / 2.0)
largeCircle.physicsBody = SKPhysicsBody(edgeChainFromPath: largeCirclePath)
addChild(largeCircle)
let smallCircle = SKShapeNode(circleOfRadius: 10)
smallCircle.position = CGPointMake(CGRectGetWidth(frame) / 2.0, CGRectGetHeight(frame) / 2.0)
smallCircle.physicsBody = SKPhysicsBody(circleOfRadius: 10)
addChild(smallCircle)

Related

Xcode - Swift SpriteKit: SKTileMapNode with a "physics gap"

I am playing around with SpriteKit and the TileMapNode and i've got an annoying problem.
That's how it should look like.
That's how its actually looking in the simulator/device.
The white spaces are terrible and i have no idea, how to get rid of them.
My Tiles are about 70x70 in the "Sprite Atlas - Part" of the assets, i've configured my tilemapnode with a scale of 0.5 and tile size of 70x70.
While testing some cases, i figured out that this part of code triggers the error, but i have no idea, what could be wrong. Changing the SKPhysicsBody size to a smaller one, did not helped.
guard let tilemap = childNode(withName: "LevelGround") as? SKTileMapNode else { return }
let tileSize = tilemap.tileSize
let halfWidth = CGFloat(tilemap.numberOfColumns) / 2.0 * tileSize.width
let halfHeight = CGFloat(tilemap.numberOfRows) / 2.0 * tileSize.height
for row in 0..<tilemap.numberOfRows {
for col in 0..<tilemap.numberOfColumns {
if tilemap.tileDefinition(atColumn: col, row: row) != nil {
let x = CGFloat(col) * tileSize.width - halfWidth
let y = CGFloat(row) * tileSize.height - halfHeight
let rect = CGRect(x: 0, y: 0, width: tileSize.width, height: tileSize.height)
let tileNode = SKShapeNode(rect: rect)
tileNode.position = CGPoint(x: x, y: y)
tileNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 70, height: 70), center: CGPoint(x: tileSize.width / 2.0, y: tileSize.height / 2.0))
tileNode.physicsBody?.isDynamic = false
tileNode.physicsBody?.collisionBitMask = 2
tileNode.physicsBody?.categoryBitMask = 1
tileNode.physicsBody?.contactTestBitMask = 2 | 1
tileNode.name = "Ground"
tilemap.addChild(tileNode)
}
}
}
tileNode.strokeColor = .clear
solved the problem
Update:
problem not solved... just moved :/
When checking, if ground & player are in contact, every new tile the status is switching between "contact" and "no contact".
When using a cube instead a circle, the cube begins to rotate. It seems, the corner of the cube get's stuck at the minimal space between the tiles.

StrokeEnd not drawing full circle

I'm trying to build a progress view for a Music Player. I am totaly new to programming in general and now really reached a dead end here.
My progress works fine. I am animatig "strokeEnd" with the value (value/duration) of the Mediafile, but strokeEnd only ever reaches have of the circle. However, the song is then finished as well, this is why I think, somewhere it calculats wrong.
It is a very simple Shaplayer I am using.
var value: CGFloat = 0.0{
didSet {
redrawStrokeEnd() }
}
private func setupShapeLayer(shapeLayer: CAShapeLayer) {
let startAngle = CGFloat(M_PI_2)
let endAngle = CGFloat(M_PI_2 * 2 + M_PI_2)
progressLayer.path = UIBezierPath(arcCenter:centerPoint, radius: CGRectGetWidth(frame)/2 + 5, startAngle:startAngle, endAngle: endAngle, clockwise: true).CGPath
progressLayer.strokeStart = 0.0
progressLayer.strokeEnd = 0.0 }
func redrawStrokeEnd() {
progressLayer.strokeEnd = CGFloat(value / CGFloat(duration))}
value = currentTime and duration = Playbackduration.
I can't figure out, why stroke End does not move the full circle.
Perhaps you're using the wrong constant? As some of the other commenters have pointed out, a circle in radians traverses 2 * pi. However the constant M_PI_2 is equal to (pi / 2) So right now you're only traversing from (pi/2) -> (3pi/2)... or pi. If you really want to start at pi/2 try:
let startAngle = CGFloat(M_PI_2)
let endAngle = CGFloat(M_PI_2 + 2.0 * M_PI)
It's not strokeEnd the reason you see half circle. You just have wrong start and end angles for your path. You can set them as below to have a full circle :
let startAngle = CGFloat(M_PI_2)
let endAngle = CGFloat(5*M_PI_2)
Or better if you don't want to set angles by radians set them in degrees like this :
let startAngle = CGFloat(0 * M_PI / 180)
let endAngle = CGFloat(360 * M_PI / 180.0)
Just changing the start angle and end angle in your code will work for you.
let startAngle = -90.0
let endAngle = -90.01

Sprite-kit: How to position sprites on a circular path

So I am experimenting with Sprite-kit, to build circular path where the main character can follow and collect coins. I have successfully positioned my character and made him follow my circular path.
What I'm trying to achieve is the following:
red ball is the main character [done]
white polygons are the coins
// Adding the big circle
let runway = SKSpriteNode(imageNamed: "runway")
runway.position = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame))
addChild(runway)
// Adding the player
player = SKSpriteNode(imageNamed: "player")
player.position = CGPointMake( CGRectGetMidX(frame) , (CGRectGetMidY(frame) + runway.size.width/2) )
// Calculating the initial position of the player and creating a circular path around it
let dx = player.position.x - frame.width / 2
let dy = player.position.y - frame.height / 2
let radian = atan2(dy, dx)
let playerPath = UIBezierPath(
arcCenter: CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMidY(frame)),
radius: (runway.frame.size.width / 2) - 20,
startAngle: radian,
endAngle: radian + CGFloat(M_PI * 4.0),
clockwise: true)
let follow = SKAction.followPath(playerPath.CGPath, asOffset: false, orientToPath: true, speed: 200)
player.runAction(SKAction.repeatActionForever(follow))
My problem now, is how to position my coins on that same path ??
Is there a way to generate their positions using the same path or should I create a specific path for each coin and extract the path currentPoint each time ??
Is there a simpler way to solving my problem ??
Thanks
Like I said, you need to know where is the center of the path (in your case that is CGPoint(x: frame.midX, y:frame.midY) which is "center" of the screen) and you have to know the radius (you have it already calculated when you was creating the path) and you need an angle that the ray from center (frame.midX,frame.midY) to the point on the circumference (coinX,coinY) makes with positive x axis:
import SpriteKit
class GameScene: SKScene {
let player = SKSpriteNode(color: .purpleColor(), size: CGSize(width: 30, height: 30))
override func didMoveToView(view: SKView) {
/* Setup your scene here */
// Adding the big circle
let runway = SKSpriteNode(color: .orangeColor(), size: CGSize(width: 150, height: 150))
runway.position = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame))
addChild(runway)
// Adding the player
player.position = CGPointMake( CGRectGetMidX(frame) , (CGRectGetMidY(frame) + runway.size.width/2) )
addChild(player)
// Calculating the initial position of the player and creating a circular path around it
let dx = player.position.x - frame.width / 2
let dy = player.position.y - frame.height / 2
let radius = (runway.frame.size.width / 2) - 20.0
let radian = atan2(dy, dx)
let playerPath = UIBezierPath(
arcCenter: CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMidY(frame)),
radius: radius,
startAngle: radian,
endAngle: radian + CGFloat(M_PI * 4.0),
clockwise: true)
let follow = SKAction.followPath(playerPath.CGPath, asOffset: false, orientToPath: true, speed: 200)
player.runAction(SKAction.repeatActionForever(follow))
let numberOfCoins = 8
for i in 0...numberOfCoins {
let coin = SKSpriteNode(color: .yellowColor(), size: CGSize(width: 10, height: 10))
let angle = 2 * M_PI / Double(numberOfCoins) * Double(i)
let coinX = radius * cos(CGFloat(angle))
let coinY = radius * sin(CGFloat(angle))
coin.position = CGPoint(x:coinX + frame.midX, y:coinY + frame.midY)
addChild(coin)
}
}
}
Just a sidenote : In Sprite-Kit the angle of 0 radians specifies the positive x axis. And the positive angle is in the counterclockwise direction. The source - Building Your Scene.
The answer that Whirlwind posted is valid, but needs to be migrated to work with Xcode 11.
The following is an updated version of that answer, along with an animation showing it in action.
override func didMove(to view: SKView) {
/* Setup your scene here */
// Adding the big circle
let runway = SKSpriteNode(color: .orange, size: CGSize(width: 150, height: 150))
runway.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(runway)
// Adding the player
player.position = CGPoint(x: frame.midX, y: frame.midY + runway.size.width / 2)
addChild(player)
// Calculating the initial position of the player and creating a circular path around it
let dx = player.position.x - frame.width / 2
let dy = player.position.y - frame.height / 2
let radius = (runway.frame.size.width / 2) - 20.0
let radian = atan2(dy, dx)
let playerPath = UIBezierPath(
arcCenter: CGPoint(x: frame.midX, y: frame.midY),
radius: radius,
startAngle: radian,
endAngle: radian + CGFloat(.pi * 4.0),
clockwise: true)
let follow = SKAction.follow(playerPath.cgPath, asOffset: false, orientToPath: true, speed: 200)
player.run(SKAction.repeatForever(follow))
let numberOfCoins = 8
for i in 0...numberOfCoins {
let coin = SKSpriteNode(color: .yellow, size: CGSize(width: 10, height: 10))
let angle = 2 * .pi / Double(numberOfCoins) * Double(i)
let coinX = radius * cos(CGFloat(angle))
let coinY = radius * sin(CGFloat(angle))
coin.position = CGPoint(x:coinX + frame.midX, y:coinY + frame.midY)
addChild(coin)
}
}
How it appears in action:

Better performance of UIBezierPath drawing iOS Swift ?

In my app I'm doing some drawing, but the way I'm doing it is very inefficient and updating the view takes long time. I'm just curious of there is a better way to achieve this. The image and the code are below.
Any kind of help is highly appreciated.
func drawDialWithCurrentValue( currentValue:CGFloat, andNewValue newValue:CGFloat)
{
let radius = 500.0
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 0,y: radius),
radius: CGFloat(radius),
startAngle: CGFloat(0),
endAngle:CGFloat(M_PI * 2),
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.CGPath
shapeLayer.fillColor = UIColor.blueColor().CGColor
shapeLayer.strokeColor = UIColor.blueColor().CGColor
shapeLayer.lineWidth = 3.0
drawView.layer.addSublayer(shapeLayer)
let x0 = 0.0
let y0 = radius
var x1 = x0
var y1 = y0
var degsInRadians = 0.0
for var index = 0; index < 360; index++
{
if index%2 == 0
{
degsInRadians = Double(index) * (M_PI/180)
x1 = x0 + radius * cos(degsInRadians)
y1 = y0 + radius * sin(degsInRadians)
let linePath = UIBezierPath()
linePath.moveToPoint(CGPoint( x: x1, y: y1))
linePath.addLineToPoint(CGPoint( x:0, y:radius))
let shapeLayerLine = CAShapeLayer()
shapeLayerLine.path = linePath.CGPath
shapeLayerLine.fillColor = UIColor.clearColor().CGColor
shapeLayerLine.strokeColor = UIColor.greenColor().CGColor
shapeLayerLine.lineWidth = 3.0
drawView.layer.addSublayer(shapeLayerLine)
}
}
let innerCirc = UIBezierPath(arcCenter: CGPoint(x: 0,y: radius),
radius: CGFloat(radius - 100),
startAngle: CGFloat(0),
endAngle:CGFloat(M_PI * 2),
clockwise: true)
let shapeLayer2 = CAShapeLayer()
shapeLayer2.path = innerCirc.CGPath
shapeLayer2.fillColor = UIColor.blueColor().CGColor
shapeLayer2.strokeColor = UIColor.blueColor().CGColor
shapeLayer2.lineWidth = 3.0
drawView.layer.addSublayer(shapeLayer2)
}
You can do this with only two layers. One for the line segments and one for the background circle (which has a different stroke color).
This function will make a single BezierPath for all the line segments (for your top layer). If you are not going to be scaling at every draw, you should set keep a single copy of the path in a static (or instance) variable and reuse it. Even if you do resize the shape, you should only recalculate the path when the radius actually changes.
func clockSegments(count:Int, radius:CGFloat, length:CGFloat)->UIBezierPath
{
let allSegments = UIBezierPath()
let baseSegment = UIBezierPath()
// baseSegment is horizontal movement from origin, followed by line up to radius
// (0,0).....................---------
baseSegment.moveToPoint( CGPointMake(radius-length, 0.0) )
baseSegment.addLineToPoint( CGPointMake(radius, 0.0) )
baseSegment.lineWidth = 3
// rotate and add segments
let segmentAngle = 2 * CGFloat(M_PI) / CGFloat(count)
let rotate = CGAffineTransformMakeRotation(segmentAngle)
for _ in 1...count
{
allSegments.appendPath(baseSegment)
baseSegment.applyTransform(rotate)
}
return allSegments
}
to get the segment path for the parameters you used in your example you can call the function like this:
layer.path = clockSegments(360, radius:500, length:100)
On an iPad3, using SpriteKit, rendering choked at 200 segments but if I used two shape nodes with 180 each, it had no problem. This may hint at a practical limit to the number of elements in the BezierPath. If you encounter this issue, you could add a parameter to offset the starting segment and use two 180 segment layers (one being rotated by 1 degree).

iOS Swift Xcode 6: CGAffineTransformRotate with auto-layout anchorpoint

I'm making an app with a rotatable pie chart. However, my pie chart rotates around (0,0) and I can't find a solution to make it rotate around its center.
Some code:
// DRAWING CODE
// Set constants for piePieces
let radius: CGFloat = CGFloat(screen.width * 0.43)
let pi: CGFloat = 3.1415926535
let sliceRad: CGFloat = 2.0 * pi / CGFloat(categoryArray.count)
var currentAngle: CGFloat = -0.5 * sliceRad - 0.5 * pi
let center: CGPoint = CGPoint(x: screen.width / 2.0, y: radius)
println("Center point: \(center)")
//container!.layer.anchorPoint = CGPoint(x: screen.width, y: radius)
// Draw all pie charts, add them to container (chartView)
for category in categoryArray {
let slice = UIView()
slice.frame = self.frame
let shapeLayer = CAShapeLayer()
let scoreIndex = CGFloat(category.calculateScoreIndex())
let sliceRadius: CGFloat = scoreIndex * radius
// Draw the path
var path:UIBezierPath = UIBezierPath()
path.moveToPoint(center)
path.addLineToPoint(CGPoint(x: center.x + sliceRadius * cos(currentAngle), y: center.y + sliceRadius * sin(currentAngle)))
path.addArcWithCenter(center, radius: sliceRadius, startAngle: currentAngle, endAngle: currentAngle + sliceRad, clockwise: true)
path.addLineToPoint(center)
path.closePath()
// For next slice, add 2*pi Rad / n categories
currentAngle += sliceRad
// Add path to shapeLayer
shapeLayer.path = path.CGPath
//shapeLayer.frame = self.frame
shapeLayer.fillColor = SurveyColors().getColor(category.categoryIndex).CGColor
shapeLayer.anchorPoint = center
// Add shapeLayer to sliceView
slice.layer.addSublayer(shapeLayer)
// Add slice to chartView
container!.addSubview(slice)
}
self.addSubview(container!)
//container!.center = center
container!.layer.anchorPoint = center
}
I sheduled a NSTimer to perform a rotation every 2 seconds (for testing purposes):
func rotate() {
let t: CGAffineTransform = CGAffineTransformRotate(container!.transform, -0.78)
container!.transform = t;
}
The pie chart rotates around (0,0). What is going wrong?
I believe a good solution to your problem would be to set a container view:
container = UIView()
container!.frame = CGRect(x: 0.0, y: 0.0, width: screen.width, height: screen.width)
container!.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
container!.backgroundColor = UIColor.clearColor()
And then add all slices to this container view:
let slice = UIView()
slice.frame = CGRect(x: 0.0 , y: 0.0 , width: container!.bounds.width, height: container!.bounds.height)
let shapeLayer = CAShapeLayer()
let scoreIndex = CGFloat(category.calculateScoreIndex())
let sliceRadius: CGFloat = scoreIndex * radius
/*
*/
container!.addSubview(slice)
Also, make sure not to add your chart as a subview to a view with auto-layout constraints (which might interfere with you CGAffineTransformRotate).

Resources