I am creating a mobile application in Swift 3's SpriteKit. I am trying to create two rocks that a character has to dodge. I have the random generation working with ->
func createTopRock(){
var topRockChoice = [SKTexture(image: #imageLiteral(resourceName: "rockDown")), SKTexture(image: #imageLiteral(resourceName: "rockGrassDown")), SKTexture(image: #imageLiteral(resourceName: "rockSnowDown")), SKTexture(image: #imageLiteral(resourceName: "rockIceDown"))]
let topRock = SKSpriteNode.init(texture: topRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
topRock.zPosition = -9
topRock.name = "TopRock"
topRock.position = CGPoint(x: self.frame.width + topRock.size.width * 2, y: frame.maxY - topRock.frame.height / 2)
topRock.physicsBody = SKPhysicsBody(texture: topRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
topRock.physicsBody?.categoryBitMask = physicsCatagory.topRock
topRock.physicsBody?.collisionBitMask = physicsCatagory.plane
topRock.physicsBody?.contactTestBitMask = physicsCatagory.plane
topRock.physicsBody?.affectedByGravity = false
topRock.physicsBody?.isDynamic = false
self.addChild(topRock)
let randomNumTop = arc4random_uniform(3) + 3
spawnDelayForeverTop = Timer.scheduledTimer(timeInterval: TimeInterval(randomNumTop), target: self, selector: #selector(self.createTopRock), userInfo: nil, repeats: false)
}
func createBtmRock(){
var btmRockChoice = [SKTexture(image: #imageLiteral(resourceName: "rock")), SKTexture(image: #imageLiteral(resourceName: "rockGrass")), SKTexture(image: #imageLiteral(resourceName: "rockSnow")), SKTexture(image: #imageLiteral(resourceName: "rockIce"))]
let btmRock = SKSpriteNode.init(texture: btmRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
btmRock.zPosition = -9
btmRock.position = CGPoint(x: self.frame.width, y: frame.minY + btmRock.frame.height / 2)
btmRock.name = "BtmRock"
btmRock.physicsBody = SKPhysicsBody(texture: btmRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
btmRock.physicsBody?.categoryBitMask = physicsCatagory.topRock
btmRock.physicsBody?.collisionBitMask = physicsCatagory.plane
btmRock.physicsBody?.contactTestBitMask = physicsCatagory.plane
btmRock.physicsBody?.affectedByGravity = false
btmRock.physicsBody?.isDynamic = false
self.addChild(btmRock)
let randomNumBtm = arc4random_uniform(2) + 1
spawnDelayForeverBtm = Timer.scheduledTimer(timeInterval: TimeInterval(randomNumBtm), target: self, selector: #selector(createBtmRock), userInfo: nil, repeats: false)
}
As you can see from the code everything works. The one thing I do not want to do is have it like Flappy Bird and have the btmRock and topRock to have the same position, so to prevent that I have added the timeIntervals to be different. This works to an extent; but the player still runs into some rocks that are nearly impossible to go through.
I feel like I am going about this wrong and I don't know how to fix this. I want them to be close enough sometimes to be hard but not nearly impossible. When I tried to implement a bool system to when a top rock is created and is within a certain distance don't create a bottom rock. What I ran into trying to do this is that I would never have a bottom rock after that occurred. Thanks in advance.
Personally I wouldn't use Timer in my SpriteKit game (for reasons that you can find all over SO). A very simple solution could be to call the generation of the rocks from your Update func.
I would also create a base bottom rock and base top rock and make copies of it as needed, because creating physics objects on the fly can get expensive (or I would create an array of the objects at start and pull from the array as needed)
P.S. I wrote this off the cuff, so I cannot guarantee typo's don't exist ;)
var genInterval = 4 //How many seconds between rocks on bottom or top (not between top & bottom. that will be half of this value)
var genOffset = genInterval / 2 //offset the bottom rocks half of the interval so that they do't line up
var bottomRock: SKSpriteNode! //your base bottom rock for copying
var topRock: SKSpriteNode! //your base top rock for copying
var updateTopTime: Double = 0
var updateBottomTime: Double = 0
func setupRocks() {
//create the base bottom rock
var btmRockChoice = [SKTexture(image: #imageLiteral(resourceName: "rock")), SKTexture(image: #imageLiteral(resourceName: "rockGrass")), SKTexture(image: #imageLiteral(resourceName: "rockSnow")), SKTexture(image: #imageLiteral(resourceName: "rockIce"))]
bottomRock = SKSpriteNode(texture: btmRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
btmRock.zPosition = -9
btmRock.position = CGPoint(x: self.frame.width, y: frame.minY + btmRock.frame.height / 2)
btmRock.name = "BtmRock"
btmRock.physicsBody = SKPhysicsBody(texture: btmRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
btmRock.physicsBody?.categoryBitMask = physicsCatagory.topRock
btmRock.physicsBody?.collisionBitMask = physicsCatagory.plane
btmRock.physicsBody?.contactTestBitMask = physicsCatagory.plane
btmRock.physicsBody?.affectedByGravity = false
btmRock.physicsBody?.isDynamic = false
//create the base top rock
var topRockChoice = [SKTexture(image: #imageLiteral(resourceName: "rockDown")), SKTexture(image: #imageLiteral(resourceName: "rockGrassDown")), SKTexture(image: #imageLiteral(resourceName: "rockSnowDown")), SKTexture(image: #imageLiteral(resourceName: "rockIceDown"))]
topRock = SKSpriteNode(texture: topRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
topRock.zPosition = -9
topRock.name = "TopRock"
topRock.position = CGPoint(x: self.frame.width + topRock.size.width * 2, y: frame.maxY - topRock.frame.height / 2)
topRock.physicsBody = SKPhysicsBody(texture: topRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
topRock.physicsBody?.categoryBitMask = physicsCatagory.topRock
topRock.physicsBody?.collisionBitMask = physicsCatagory.plane
topRock.physicsBody?.contactTestBitMask = physicsCatagory.plane
topRock.physicsBody?.affectedByGravity = false
topRock.physicsBody?.isDynamic = false
}
override func update(_ currentTime: CFTimeInterval) {
//optional prevents generation if game is not playing
guard gameState == .playing else { return }
if updateTopTime == 0 {
updateTopTime = currentTime
}
if updateBottomTime == 0 {
updateBottomTime = currentTime
}
if currentTime - updateBottomTime > genOffset {
createBtmRock()
genOffset = genInterval
updateBottomTime = currentTime
}
else if currentTime - updateTopTime > genInterval {
createTopRock()
updateTopTime = currentTime
}
}
func createTopRock() {
//You can make this number a class variable to increase the rate as the game progresses
let randomNum = arc4random_uniform(3)
//there is a 1 in 3 chance that this rock will get created
if randomNum == 1 {
let rock = topRock.copy as! SKSpriteNode()
self.addChild(rock)
}
}
func createBtmRock() {
//You can make this number a class variable to increase the rate as the game progresses
let randomNum = arc4random_uniform(2)
//there is a 1 in 2 chance that this rock will get created
if randomNum == 0 {
let rock = bottomRock.copy as! SKSpriteNode()
self.addChild(rock)
}
}
The logic in your final paragraph seems like a plausible solution. You will need to implement some kind of custom logic to prevent a top and bottom rock from spawning within some set distance.
Inside your createBtmRock function, when you set the new rock's position, check to see if it is close to any existing top rocks. You can determine closeness by comparing the position property of the new rock to all existing rocks.
If you find the new rock would be too close to existing rocks, don't add the new rock to the scene, and continue the timer to make sure you generate bottom rocks in the future.
This is a simple solution to your immediate problem. You may want to look into procedural generation strategies for generating your rocks. If you want to refine the behavior later (by making rocks harder to pass as the game progresses, for example), you will need a more permanent solution.
Consider creating a separate function that will generate positions for your rocks. You can handle all of the position generation logic separately and use the result to position your rocks.
func generateRockPositions() -> [CGPoint]
You could do this in advance when the user starts the game, or generate positions as the game progresses. This will allow you to make changes to your procedural generation algorithm in the future more easily.
Hopefully that helps, best of luck with your game!
Related
So I'm creating a game in Swift with Spritekit and have ran into an issue with getting my game to work. I'm still a beginner with programming so I've likely missed out on a solution to this myself.
Anyway, so the game concept is a simple arcade vertical scroller that involves a player trying to dodge platforms as it descends downward. The mechanics (so far) are a stationary player on the y axis that can move left and right along the x axis while the platforms scroll upward along with the background moving with the platforms to give a visual effect of descent. I've gotten a build working to be fully playable, but there's an issue with spawning the platforms perfectly spaced out. Here's a sketch:
Concept Image
The picture on the left what I'm trying to achieve, while the one on the right is my current and flawed method. The main issue with the one on the right, is that it uses a collision to trigger spawning which means the spawn trigger node (red line) has to be 1 pixel tall to allow for perfect spacing. If the spawn trigger node is more than 1 pixel tall, then the collision may not trigger on that the first pixel of contact and trigger the node a few pixels deep which throws off the entire spacing. Also if the spawn trigger is only 1 pixel tall, it often won't trigger unless the everything is scrolling at slow speeds.
I've tried to think of other methods to approach this but I'm at a loss. I cannot use a simple timer to spawn nodes at intervals because the speed at which the game scrolls is variable and is constantly changing by player controls. The only two other options I can think of (which I don't know how to do either) is either spawn node sets at fixed y-positions and set that on a loop, or change it so the player is actually descending downward while everything is generating around it (seems tougher and maybe unnecessary). I'm considering just rewriting my createPlatforms() method if I need to, but here's the code for that and the background anyway:
var platformGroup = Set<SKSpriteNode>()
var platformSpeed: CGFloat = 0.6 { didSet { for platforms in platformGroup { platforms.speed = platformSpeed } } }
var platformTexture: SKTexture!
var platformPhysics: SKPhysicsBody!
var platformCount = 0
var backgroundPieces: [SKSpriteNode] = [SKSpriteNode(), SKSpriteNode()]
var backgroundSpeed: CGFloat = 1.0 { didSet { for background in backgroundPieces { background.speed = backgroundSpeed } } }
var backgroundTexture: SKTexture! { didSet { for background in backgroundPieces { background.texture = backgroundTexture } } }
func createPlatforms() {
let min = CGFloat(frame.width / 12)
let max = CGFloat(frame.width / 3)
var xPosition = CGFloat.random(in: -min ... max)
if platformCount >= 20 && platformCount < 30 {
stage = 0
setTextures()
xPosition = frame.size.width * 0.125
} else if platformCount == 30 {
stage = 2
setTextures()
} else if platformCount >= 50 && platformCount < 60 {
stage = 0
setTextures()
xPosition = 184
} else if platformCount == 60 {
stage = 3
setTextures()
}
platformPhysics = SKPhysicsBody(rectangleOf: CGSize(width: platformTexture.size().width, height: platformTexture.size().height))
let platformLeft = SKSpriteNode(texture: platformTexture)
platformLeft.physicsBody = platformPhysics.copy() as? SKPhysicsBody
platformLeft.physicsBody?.isDynamic = false
platformLeft.physicsBody?.affectedByGravity = false
platformLeft.physicsBody?.collisionBitMask = 0
platformLeft.scale(to: CGSize(width: platformLeft.size.width * 3, height: platformLeft.size.height * 3))
platformLeft.zPosition = 20
platformLeft.name = "platform"
platformLeft.speed = platformSpeed
let platformRight = SKSpriteNode(texture: platformTexture)
platformRight.physicsBody = platformPhysics.copy() as? SKPhysicsBody
platformRight.physicsBody?.isDynamic = true
platformRight.physicsBody?.collisionBitMask = 0
platformRight.scale(to: CGSize(width: platformRight.size.width * 3, height: platformRight.size.height * 3))
platformRight.zPosition = 20
platformRight.name = "platform"
platformRight.speed = platformSpeed
let scoreNode = SKSpriteNode(color: UIColor.clear, size: CGSize(width: frame.width, height: 32))
scoreNode.physicsBody = SKPhysicsBody(rectangleOf: scoreNode.size)
scoreNode.physicsBody?.isDynamic = false
scoreNode.zPosition = 100
scoreNode.name = "scoreDetect"
scoreNode.speed = platformSpeed
let platformTrigger = SKSpriteNode(color: UIColor.orange, size: CGSize(width: frame.width, height: 4))
platformTrigger.physicsBody = SKPhysicsBody(rectangleOf: platformTrigger.size)
platformTrigger.physicsBody?.isDynamic = true
platformTrigger.physicsBody?.affectedByGravity = false
platformTrigger.physicsBody?.categoryBitMask = Collisions.detect
platformTrigger.physicsBody?.contactTestBitMask = Collisions.spawn
platformTrigger.physicsBody?.collisionBitMask = 0
platformTrigger.physicsBody?.usesPreciseCollisionDetection = true
platformTrigger.zPosition = 100
platformTrigger.name = "platformTrigger"
platformTrigger.speed = platformSpeed
let newNodes: Set<SKSpriteNode> = [platformLeft, platformRight, scoreNode, platformTrigger]
for node in newNodes {
platformGroup.insert(node)
}
let yPosition = spawnNode.position.y - transitionPlatform.size().height
let gapSize: CGFloat = -frame.size.width / 6
print(gapSize)
platformLeft.position = CGPoint(x: xPosition + platformLeft.size.width - gapSize, y: -yPosition)
platformRight.position = CGPoint(x: xPosition + gapSize, y: -yPosition)
scoreNode.position = CGPoint(x: frame.midX, y: platformLeft.position.y - platformLeft.size.height / 2)
platformTrigger.position = CGPoint(x: frame.midX, y: platformLeft.position.y)
print(platformLeft.position.y)
print(platformLeft.frame.midY)
let endPosition = frame.maxY + frame.midY
let moveAction = SKAction.moveBy(x: 0, y: endPosition, duration: 7)
for node in newNodes {
let moveSequence = SKAction.sequence([
moveAction,
SKAction.removeFromParent(),
SKAction.run {
self.platformGroup.remove(node)
}
])
addChild(node)
nodeArray.append(node)
node.run(moveSequence)
}
platformCount += 1
}
func startPlatforms() {
let create = SKAction.run { [unowned self] in
self.createPlatforms()
}
run(create)
}
func createBackground() {
for i in 0 ... 1 {
let background = backgroundPieces[i]
background.texture = backgroundTexture
background.anchorPoint = CGPoint(x: 0, y: 0)
background.zPosition = -5
background.size = CGSize(width: frame.size.width, height: frame.size.width * 2.5)
background.position = CGPoint(x: 0, y: background.size.height + (-background.size.height) + (-background.size.height * CGFloat(i)))
self.addChild(background)
nodeArray.append(background)
let scrollUp = SKAction.moveBy(x: 0, y: background.size.height, duration: 5)
let scrollReset = SKAction.moveBy(x: 0, y: -background.size.height, duration: 0)
let scrollLoop = SKAction.sequence([scrollUp, scrollReset])
let scrollForever = SKAction.repeatForever(scrollLoop)
background.run(scrollForever)
}
}
Does anybody have any suggestions on how I approach this or change it so it would work perfectly everytime?
What I'm trying to do is update my SKSpriteNodes so I can change their scrolling speeds dynamically, however they aren't really working consistently. I didn't include the code, but I have another method with a switch case that sets the value of platformSpeed whenever the state is changed (in this case, the switch case is changed with UIButtons). In my code I have an SKSpriteNode array and a platformSpeed property that includes didSet so my value is updated properly.
In my method to create the platforms, I grouped my SpriteNodes into platformGroup then looped through them with addChild(). Not sure why it's acting this way but here's a quick video of what it looks like in action:
demonstration clip
So with the buttons I'm changing the switch case, and as you can see, not all of the nodes speeds are updating properly and some get faster than others and eventually pass them. I need them to stay equal distance between each other.
Now here's my code:
class GameScene: SKScene, SKPhysicsContactDelegate {
var platformGroup = [SKSpriteNode]()
var platformSpeed: CGFloat = 1.0 {
didSet {
for platforms in platformGroup {
platforms.speed = platformSpeed
}
}
}
let platformTexture = SKTexture(imageNamed: "platform")
var platformPhysics: SKPhysicsBody!
func createPlatforms() {
let platformLeft = SKSpriteNode(texture: platformTexture)
platformLeft.physicsBody = platformPhysics.copy() as? SKPhysicsBody
platformLeft.physicsBody?.isDynamic = false
platformLeft.scale(to: CGSize(width: platformLeft.size.width * 4, height: platformLeft.size.height * 4))
platformLeft.zPosition = 20
let platformRight = SKSpriteNode(texture: platformTexture)
platformRight.physicsBody = platformPhysics.copy() as? SKPhysicsBody
platformRight.physicsBody?.isDynamic = false
platformRight.scale(to: CGSize(width: platformRight.size.width * 4, height: platformRight.size.height * 4))
platformRight.zPosition = 20
let scoreNode = SKSpriteNode(color: UIColor.clear, size: CGSize(width: frame.width, height: 32))
scoreNode.physicsBody = SKPhysicsBody(rectangleOf: scoreNode.size)
scoreNode.physicsBody?.isDynamic = false
scoreNode.name = "scoreDetect"
scoreNode.zPosition = 40
platformGroup = [platformLeft, platformRight, scoreNode]
let yPosition = frame.width - platformRight.frame.width
let max = CGFloat(frame.width / 4)
let xPosition = CGFloat.random(in: -80...max)
let gapSize: CGFloat = -50
platformLeft.position = CGPoint(x: xPosition + platformLeft.size.width - gapSize, y: -yPosition)
platformRight.position = CGPoint(x: xPosition + gapSize, y: -yPosition)
scoreNode.position = CGPoint(x: frame.midX, y: yPosition - (scoreNode.size.width / 1.5))
let endPosition = frame.maxY + (platformLeft.frame.height * 3)
let moveAction = SKAction.moveBy(x: 0, y: endPosition, duration: 7)
let moveSequence = SKAction.sequence([moveAction, SKAction.removeFromParent()])
for platforms in platformGroup {
addChild(platforms)
platforms.run(moveSequence)
}
platformCount += 1
}
func loopPlatforms() {
let create = SKAction.run { [unowned self] in
self.createPlatforms()
platformCount += 1
}
let wait = SKAction.wait(forDuration: 1.1)
let sequence = SKAction.sequence([create, wait])
let repeatForever = SKAction.repeatForever(sequence)
run(repeatForever)
}
I think I can see what's going wrong. When you change platformSpeed, it changes the speed of all the platforms in platformGroup. And createPlatforms() is being called multiple times. Now, each time it's called you create a pair of platforms and assign these to platformGroup. Since you call the function multiple times, it's overwriting any existing values in the array. That's why changing platformSpeed only updates the speed of the latest platforms you've created---the older platforms stay the same speed because they're not in platformGroup anymore.
To fix this, my advice would be to have platformGroup store all the platforms currently on the screen. You could do this by changing
platformGroup = [platformLeft, platformRight, scoreNode]
to something like
let newNodes = [platformLeft, platformRight, scoreNode]
platformGroup += newNodes
// Alternatively, platformGroup.append(contentsOf: newNodes)
Now you need to make sure you're 1) only adding the new nodes to the scene, and 2) removing the old nodes from platformGroup when they're removed from the parent. You could do this by changing
let moveAction = SKAction.moveBy(x: 0, y: endPosition, duration: 7)
let moveSequence = SKAction.sequence([moveAction, SKAction.removeFromParent()])
for platforms in platformGroup {
addChild(platforms)
platforms.run(moveSequence)
}
to something like
let moveAction = SKAction.moveBy(x: 0, y: endPosition, duration: 7)
for node in newNodes {
let moveSequence = SKAction.sequence([
moveAction,
SKAction.removeFromParent(),
SKAction.run {
self.platformGroup.remove(node)
}
])
addChild(node)
node.run(moveSequence)
}
Now that you're keeping a track of all platforms ever made, your speed changes should be applied consistently to every platform on the screen. Hope this works!
Working on a game right now, I've faced a problem regarding management of SkSpriteNodes. I have a SpriteNode whose texture size is lower than physicsBody size assigned to it. It is possible to move, thanks to something similar to an SKAction, only the texture of a node and not its physicsBody?
To explain the problem in other terms, I will give you a graphic example:
As you can see, what I want to achieve is not to modify physicsBody proprieties(in order to not affect collision or having problems with continuos reassigning of PhysicsBody entities), but changing its texture length and adjusting its position. How can I achieve this programmatically?
A bit of code for context, which is just illustrative of the problem:
let node = SKSpriteNode(color: .red, size: CGSize(width: 8, height: 60)
node.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 8, height: 60)
self.addChild(node)
//what I've tried is something like that
//It causes glitches in visualisation... and I need to move the object since resizing is towards the center.
let resize = SKAction.scaleY(to: 0.5, duration: 5)
let move = SKAction.move(to: node.position - CGPoint(x:0, y:node.size.height*0.5), duration: 5)
let group = SKAction.group([resize, move])
node.run(group)
//And this is even worse if I add, in this specific example, another point fixed to the previous node
let node2 = SKSpriteNode(color: .blue, size: CGSize(width: 8, length: 8)
node2.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 8, height: 8))
node2.position = CGPoint(x: 0, y: -node.height)
self.addChild(node2)
self.physicsWorld.add(SKPhysicsJointFixed.joint(withBodyA: node.physicsBody! , bodyB: node2.physicsBody!, anchor: CGPoint(x: 0, y: -node.size.height)))
I get your problem.
let resize = SKAction.scaleY(to: 0.5, duration: 5)
This line will cause the physicsBody to scale the x and y axis uniformly. While your texture will just scale the y axis.
Its not so straight forward changing physicsBody shapes to match actions
One way to do it though would be to call a method from
override func didEvaluateActions()
Something like this:
var group1: SKAction? = nil
var group2: SKAction? = nil
var touchCnt = 0
var test = SKSpriteNode(texture: SKTexture(imageNamed: "circle"), color: .blue, size: CGSize(width: 100, height: 100))
func setActions() {
let newPosition = CGPoint(x: -200, y: 300)
let resize1 = SKAction.scaleY(to: 0.5, duration: 5)
let move1 = SKAction.move(to: newPosition, duration: 5)
group1 = SKAction.group([resize1, move1])
let resize2 = SKAction.scaleY(to: 1, duration: 5)
let move2 = SKAction.move(to: position, duration: 5)
group2 = SKAction.group([resize2, move2])
}
func newPhysics(node: SKSpriteNode) {
node.physicsBody = SKPhysicsBody(texture: node.texture!, size: node.size)
node.physicsBody?.affectedByGravity = false
node.physicsBody?.allowsRotation = false
node.physicsBody?.usesPreciseCollisionDetection = false
}
override func sceneDidLoad() {
physicsWorld.contactDelegate = self
test.position = CGPoint(x: 0, y: 300)
setActions()
newPhysics(node: test)
addChild(test)
}
override func didEvaluateActions() {
newPhysics(node: test)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if touchCnt == 0 {
if !test.hasActions() {
test.run(group1!)
touchCnt += 1
}
} else {
if !test.hasActions() {
test.run(group2!)
touchCnt -= 1
}
}
}
if you put the above code in your gameScene, taking care to replace any duplicated methods and replacing the test node texture. then when you tap the screen the sprite should animate as you want while keeping the physics body is resized at the same time. There are a few performance issues with this though. As it changes the physics body on each game loop iteration.
This post has been modified to include a screenshot and code...
It's been four days of trying to solve this, so can someone please clearly point out what's wrong with my implementation? It's my first time lol.
Have a look at my screenshot below.
-I'm using UIKit Dynamics and am trying to trap those 3 smaller balls inside the ROUND bounds of the larger ball, but they are only being kept inside the bounds of the larger ball's rectangle.
Here's the relative code:
The large bubble is created in IB and is declared in my class:
#IBOutlet weak var homeMainBubble: UIButton!
var gravity: UIGravityBehavior!
var animator: UIDynamicAnimator!
var collision: UICollisionBehavior!
var ballOne : UIImageView!
var ballTwo : UIImageView!
var ballThree : UIImageView!
var circlePath : UIBezierPath!
override func viewDidAppear(_ animated: Bool) {
circlePath = UIBezierPath(arcCenter: CGPoint(x: homeMainBubble.frame.midX,y: homeMainBubble.frame.midY), radius: homeMainBubble.frame.width/2, startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)}
In viewDidAppear(), I create a UIBezierPath()
//Adds or removes fake balls inside 'mainBubble()' when swiped up or down---------------------
func addOrRemoveFakeBalls(fakeBalls: String) {
let ballOneIs = positionOneBubble()
let ballImages = ["playBubble", "statsBubble", "settingsBubble", "cheatsBubble"]
let ballOneImage = ballImages[ballOneIs.tag - 1] //Determines the image to used for 'fake ball 1' based on the bubble in position one
var ballTwoImage : String = ""
var ballThreeImage : String = ""
if ballOneImage == "playBubble" { //Derivitive from ballOneImage, this determines the ball to the left & ensures correct array looping.
ballTwoImage = ballImages[3]
} else {ballTwoImage = ballImages[ballOneIs.tag - 2]}
if ballOneImage == "cheatsBubble" { //Derivitive from ballOneImage, this determines the ball to the right & ensures correct array looping.
ballThreeImage = ballImages[0]
} else {ballThreeImage = ballImages[ballOneIs.tag]}
switch fakeBalls {
case "Add":
ballOne = Ellipse(frame: CGRect(x: 125, y: 50, width: largeBubbleWidth, height: largeBubbleWidth))
ballOne.image = UIImage(named: ballOneImage)
ballOne.layer.zPosition = 1
ballOne.alpha = 0.3
ballTwo = Ellipse(frame: CGRect(x: 100, y: 50, width: mediumBubbleWidth, height: mediumBubbleWidth))
ballTwo.image = UIImage(named: ballTwoImage)
ballTwo.layer.zPosition = 1
ballTwo.alpha = 0.3
ballThree = Ellipse(frame: CGRect(x: 150, y: 50, width: mediumBubbleWidth, height: mediumBubbleWidth))
ballThree.image = UIImage(named: ballThreeImage)
ballThree.layer.zPosition = 1
ballThree.alpha = 0.3
homeMainBubble.addSubview(ballOne)
homeMainBubble.addSubview(ballTwo)
homeMainBubble.addSubview(ballThree)
animator = UIDynamicAnimator(referenceView: homeMainBubble)
gravity = UIGravityBehavior(items: [ballOne, ballTwo, ballThree])
gravity.magnitude = 1
animator.addBehavior(gravity)
collision = UICollisionBehavior(items: [ballOne, ballTwo, ballThree])
collision.addBoundary(withIdentifier: "Circle" as NSCopying, for: circlePath)
collision.collisionDelegate = self
collision.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)
case "Remove":
animator.removeAllBehaviors()
gravity.removeItem(ballOne)
gravity.removeItem(ballTwo)
gravity.removeItem(ballThree)
collision.removeAllBoundaries()
ballOne.removeFromSuperview()
ballTwo.removeFromSuperview()
ballThree.removeFromSuperview()
default: break
}
}
I've temporarily added collision.translatesReferenceBoundsIntoBoundary = true so that the balls don't drop off the screen, but with that commented out, why does the 'circlePath' UIBezierPath() not hold my smaller balls inside?
I have pretty much tried every approach and had every outcome EXCEPT for what I want :P.
What am I missing in order to keep the 3 smaller balls trapped inside the large ball's image (not rectangle) bounds?
THANK YOU for saving my week! :)
If you are using UIDynamics then you can follow the advice in this other question & answer
Namely, looks like you'll want to define collisionBoundingPath to be a UIBezierPath that is round.
override public var collisionBoundingPath: UIBezierPath {
let radius = min(bounds.size.width, bounds.size.height) / 2.0
return UIBezierPath(arcCenter: CGPointZero, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
}
From there you can customize the behavior of the smaller balls hitting the outer edge through the UICollisionBehaviorDelegate (again, see linked question and answer).
Update
With more code posted, another issue I think you are having is the UICollisionBehavior isn't including the "main ball". You are only adding collision behavior between the 3 sub-balls, which is likely why they are not properly interacting with the big ball as you'd want. Hopefully that helps!
I am coding for my A2 coursework project, and making a well-known game 'Fall Down'. However i am trying to get the background to change every cycle while it scrolls - so it changes from blue to red to yellow etc. However when I run it, this only works for the first two colours, then flashes back to the first colour. Here is the code i currently have
var background = SKSpriteNode()
override func didMoveToView(view: SKView) {
let blueTexture = SKTexture(imageNamed: "BlueBackground")
let redTexture = SKTexture(imageNamed: "RedBackground")
let yellowTexture = SKTexture(imageNamed: "YellowBackground")
let greenTexture = SKTexture(imageNamed: "GreenBackground")
let purpleTexture = SKTexture(imageNamed: "PurpleBackground")
let TextureArray = [blueTexture, redTexture, yellowTexture, greenTexture, purpleTexture]
let levelProgress = SKAction.moveByX(0, y:blueTexture.size().height, duration: 5)
let newLevel = SKAction.moveByX(0 , y: -blueTexture.size().height, duration: 0)
let sequenceForever = SKAction.repeatActionForever(SKAction.sequence([ levelProgress, newLevel]))
var a = 0
for var i: CGFloat = 0; i<5; i++ {
let currentBG = TextureArray[a]
a++
background = SKSpriteNode(texture: currentBG)
background.position = CGPoint(x: CGRectGetMidX(self.frame), y: -blueTexture.size().height/2 + blueTexture.size().height * i)
background.size.width = self.frame.width
background.runAction(sequenceForever)
self.addChild(background)
}
if anyone could point me in the right direction or if any more information is needed let me know! This is my first post so any posting advice would be great too.
It might by because of this:
let levelProgress = SKAction.moveByX(0, y:blueTexture.size().height, duration: 5)
let newLevel = SKAction.moveByX(0 , y: -blueTexture.size().height, duration: 0)
let sequenceForever = SKAction.repeatActionForever(SKAction.sequence([ levelProgress, newLevel]))
background.position = CGPoint(x: CGRectGetMidX(self.frame), y: -blueTexture.size().height/2 + blueTexture.size().height * i
you always use the blueTexture.size. try to change it to a constant value.