I have a series of UIBezierPath's that I've turned into 3D objects in SceneKit. They form a long, jagged line with occasional gaps (a gap separates one object from another).
Here's what that looks like:
The problem: As I move the camera around, the material on the side of the objects flickers strangely and changes colors.
I believe what's happening is that my path-drawing code is wrong, and is somehow creating duplicate objects positioned inside the existing object(s). So, I think the flickering color is really the other, duplicate object showing through.
Here's what the flickering color showing through looks like:
To see the problem in action, the following code can be pasted directly into a new Game template Xcode project using SceneKit. You may paste it at the end of GameViewController's viewDidLoad.
var previousBezierPathPoint: CGPoint = CGPoint.zero
let numOfPointsPerLine: Int = 8
var hugePath = UIBezierPath()
var wasGap: Bool = false
let gapWidth: CGFloat = 10.0
var currentZdepth: CGFloat = CGFloat.random(in: 5.0...30.0)
for i in 0..<14 {
let pp = wasGap ? CGPoint(x: CGFloat(previousBezierPathPoint.x) + gapWidth, y: CGFloat(previousBezierPathPoint.y)) : CGPoint(x: CGFloat(previousBezierPathPoint.x), y: CGFloat(previousBezierPathPoint.y))
let isGap: Bool = i > 1 && Float.random(in: 0...100) > 60.0
if !isGap {
if wasGap || i == 0 {
hugePath = UIBezierPath()
hugePath.move(to: pp)
currentZdepth = CGFloat.random(in: 5.0...30.0)
}
for j in 1..<numOfPointsPerLine {
let point = CGPoint(x: pp.x + (CGFloat(j)*25.0), y: pp.y + CGFloat.random(in: -8.0...8.0))
hugePath.addLine(to: point)
previousBezierPathPoint = point
}
let pathRef = hugePath.cgPath.copy(strokingWithWidth: 10.0, lineCap: CGLineCap.butt, lineJoin: CGLineJoin.miter, miterLimit: 1.0)
let newPath = UIBezierPath(cgPath: pathRef.normalized())
let hugeShape = SCNShape(path: newPath, extrusionDepth: currentZdepth)
let colors = [
UIColor.green,
UIColor.yellow,
UIColor.purple,
UIColor.gray,
UIColor.darkGray
]
let tempMat = SCNMaterial()
tempMat.diffuse.contents = colors[Int.random(in: 0..<colors.count)]
let frontMat = SCNMaterial()
frontMat.diffuse.contents = UIColor.red
hugeShape.materials = [frontMat, tempMat, tempMat, tempMat, tempMat, tempMat]
let hugeNode = SCNNode()
hugeNode.geometry = hugeShape
hugeNode.position.x = 0.0
hugeNode.position.z = Float(-50.0 + (currentZdepth*0.5))
hugeNode.position.y = 0.0
scnView.scene?.rootNode.addChildNode(hugeNode)
} else {
if hugePath.isEmpty == false {
hugePath.close()
}
}
wasGap = isGap
}
Please note that this question is related to this one.
Question: How can I change my code so that there are no duplicate objects?
It looks like you are continually building your path each time through the loop...
Not entirely sure if this will give you your desired results, but...
Instead of this:
if wasGap || i == 0 {
hugePath = UIBezierPath()
hugePath.move(to: pp)
currentZdepth = CGFloat.random(in: 5.0...30.0)
}
try this:
hugePath = UIBezierPath()
hugePath.move(to: pp)
if wasGap || i == 0 {
currentZdepth = CGFloat.random(in: 5.0...30.0)
}
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!
I'm trying to make a simple game in the latest version of Xcode. I use a TileMap that is pretty simple. I'm trying to figure out how to add a physics body to each and every tile. I'm stuck at getting the position of each tile and adding an SKNode there. Any way at getting the position of each tile would be extremely helpful. All the code below is in didMove(to: view)
let tileSize = grassTileMap.tileSize
let halfWidth = CGFloat(grassTileMap.numberOfColumns) / 2.0 * tileSize.width
let halfHeight = CGFloat(grassTileMap.numberOfRows) / 2.0 * tileSize.height
for node in self.children {
if node.name == "grassTileMap" {
grassTileMap = node as! SKTileMapNode
}
for col in 0..<grassTileMap.numberOfColumns {
for row in 0..<grassTileMap.numberOfRows {
let tileDef = grassTileMap.tileDefinition(atColumn: col, row: row)
if tileDef == nil {
print("no tile here")
} else {
let hitboxTileNode = SKSpriteNode(color: UIColor.clear, size: CGSize(width: 35.5, height: 35.5))
// I need to set the position of each tile to hitBoxTileNode here
hitboxTileNode.anchorPoint = CGPoint(x: 0, y: 0)
hitboxTileNode.physicsBody = SKPhysicsBody(edgeLoopFrom: hitboxTileNode.frame)
hitboxTileNode.physicsBody?.affectedByGravity = false
hitboxTileNode.physicsBody?.isDynamic = false
hitboxTileNode.physicsBody?.pinned = false
hitboxTileNode.physicsBody?.restitution = 0
hitboxTileNode.physicsBody?.friction = 0
addChild(hitboxTileNode)
}
}
}
}
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.
I fount this solution but I can't make it into swift code
This what I try
var pattern[2]:CGFloat; this
var dashed: CGPathRef = CGPathCreateCopyByDashingPath(CGPathCreateCopyByDashingPath(path, transform, phase, lengths, count);
var myShapeNode: SKShapeNode!;
var CGPathCreateCopyByDashingPath:CGPathRef;
This is how you can draw a dashed line in swift. You can change the parameters as you want.
let bezierPath = UIBezierPath()
let startPoint = CGPointMake(0, 250)
let endPoint = CGPointMake(450, 250)
bezierPath.moveToPoint(startPoint)
bezierPath.addLineToPoint(endPoint)
var pattern : [CGFloat] = [10.0, 10.0]
let dashed = CGPathCreateCopyByDashingPath (bezierPath.CGPath, nil, 0, pattern, 2)
var shapeNode = SKShapeNode(path: dashed)
shapeNode.position = CGPointMake(100, 100)
self.addChild(shapeNode)
In swift 4:
let square = SKShapeNode(rectOf: CGSize(width: 64, height: 64))
let pattern : [CGFloat] = [4.0, 4.0]
let dashed = square.path?.copy(dashingWithPhase: 1, lengths: pattern)
let shapeNode = SKShapeNode(path: dashed!)
shapeNode.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
shapeNode.fillColor = SKColor.clear
shapeNode.strokeColor = SKColor.red
shapeNode.lineWidth = 2
self.addChild(shapeNode)
For anyone looking to work out how to apply this same principal to an SKShapeNode like I was, here is an example. A rectangle with a blue dashed line.
import SpriteKit
import GameplayKit
let square = SKShapeNode(rectOfSize: CGSize(width: 64, height: 64))
let circle = SKShapeNode(circleOfRadius: 20.0)
var pattern : [CGFloat] = [2.0, 2.0]
let dashed = CGPathCreateCopyByDashingPath (square.path, nil, 0, pattern, 2)
var shapeNode = SKShapeNode(path: dashed!)
shapeNode.fillColor = UIColor.blueColor()
shapeNode.strokeColor = UIColor.blueColor()
Swift 5
if let path = path?.copy(dashingWithPhase: 1, lengths: [5, 5]) {
let line = SKShapeNode(path: path)
line.strokeColor = .white
self.addChild(line)
}
Adding to the solution provided by Mike Glukhov above, this is my solution:
func drawTrace(pointArray: Array<CGPoint>) {
if pointArray.count > 1 {
let pattern : [CGFloat] = [10.0, 10.0]
let path = CGMutablePath.init()
// start at the first point.
path.move(to: pointArray[0])
// now add all of the others.
for p in 1 ..< pointArray.count {
path.addLine(to: pointArray[p])
}
// create the dashed path.
let dashedPath = path.copy(dashingWithPhase: 1, lengths: pattern)
let dashName = "dash"
// now create the node
let line = SKShapeNode(path: dashedPath)
line.strokeColor = .white
line.name = dashName
if let parent = self.parent {
if let oldLine = parent.childNode(withName: dashName) {
oldLine.removeFromParent()
}
parent.addChild(line)
}
}
}