Drawing dashed line in Sprite Kit using SKShapeNode - swift

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)
}
}
}

Related

Why does my 3D extruded UIBezierPath have duplicates?

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)
}

How can I add my treat node in multiple places throughout the game?

I want to have about 15 treats and I just want to change the x position of these treats. I don't want to copy and paste this code 15 times, so how can I go about doing this?
func createTreatNode() {
let atlas = SKTextureAtlas(named: "Treat")
let q1 = atlas.textureNamed("treat1.png")
let q2 = atlas.textureNamed("treat2.png")
let q3 = atlas.textureNamed("treat3.png")
let treatAnimation = [q1, q2, q3]
let treat = SKSpriteNode(texture: q1)
treat.position = CGPoint(x: 1000, y: 150)
print("treat position \(treat.position)")
treat.size = CGSize(width: 60, height: 50)
treat.name = "Treat"
let animate = SKAction.animate(with: treatAnimation, timePerFrame: 0.2)
let repeatAction = SKAction.repeatForever(animate)
treat.run(repeatAction)
let body = SKPhysicsBody(rectangleOf: treat.size)
body.usesPreciseCollisionDetection = true
body.affectedByGravity = false
body.collisionBitMask = 0x7FFFFFFF
body.contactTestBitMask = 0x80000000
treat.physicsBody = body
addChild(treat)
}
I recommend adding a parameter to your createTreatNode() function. Then change treat.position = CGPoint(x: 1000, y: 150) to treat.position = CGPoint(x: atXCoordinate, y: 150) like this:
func createTreatNode(atXCoordinate: Int) {
let atlas = SKTextureAtlas(named: "Treat")
let q1 = atlas.textureNamed("treat1.png")
let q2 = atlas.textureNamed("treat2.png")
let q3 = atlas.textureNamed("treat3.png")
let treatAnimation = [q1, q2, q3]
let treat = SKSpriteNode(texture: q1)
treat.position = CGPoint(x: atXCoordinate, y: 150)
print("treat position \(treat.position)")
treat.size = CGSize(width: 60, height: 50)
treat.name = "Treat"
let animate = SKAction.animate(with: treatAnimation, timePerFrame: 0.2)
let repeatAction = SKAction.repeatForever(animate)
treat.run(repeatAction)
let body = SKPhysicsBody(rectangleOf: treat.size)
body.usesPreciseCollisionDetection = true
body.affectedByGravity = false
body.collisionBitMask = 0x7FFFFFFF
body.contactTestBitMask = 0x80000000
treat.physicsBody = body
addChild(treat)
}
This way you can call createTreatNode(atXCoordinate:) 15 times with different x coordinates and it will add treat child nodes to the current scene at the x coordinates you passed to it.

How do I flip over a UIBezierPath or cgPath thats animated onto the CAShapeLayer?

I have an array of type [UIBezierPath] that I transform into cgPaths and then animate onto a CAShapeLayer I called shapeLayer. Now for some reason all my paths are upside down, so all the paths get drawn upside down. How can I fix this, I tried a couple of methods but sadly none of them worked... I did however figure out how to scale the path. This is my code to draw the swiftPath, which is a path made up of UIBezierPaths found in the Forms class under the function swiftBirdForm(). Drawing the path is working fine, I just can't figure out how to flip it 180 degrees.
#objc func drawForm() {
var swiftPath = Forms.swiftBirdForm()
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 1
shapeLayer.frame = CGRect(x: -120, y: 120, width: 350, height: 350)
var paths: [UIBezierPath] = swiftPath
guard let path = paths.first else {
return
}
paths.dropFirst()
.forEach {
path.append($0)
}
shapeLayer.transform = CATransform3DMakeScale(0.6, 0.6, 0)
shapeLayer.path = path.cgPath
self.view.layer.addSublayer(shapeLayer)
let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimation.duration = 1.0
strokeEndAnimation.fromValue = 0.0
strokeEndAnimation.toValue = 1.0
shapeLayer.add(strokeEndAnimation, forKey: nil)
}
Using CATransform3D
shapeLayer.transform = CATransform3DMakeScale(1, -1, 1)
Transforming path,
let shapeBounds = shapeLayer.bounds
let mirror = CGAffineTransform(scaleX: 1,
y: -1)
let translate = CGAffineTransform(translationX: 0,
y: shapeBounds.size.height)
let concatenated = mirror.concatenating(translate)
bezierPath.apply(concatenated)
shapeLayer.path = bezierPath.cgPath
Transforming layer,
let shapeFrame = CGRect(x: -120, y: 120, width: 350, height: 350)
let mirrorUpsideDown = CGAffineTransform(scaleX: 1,
y: -1)
shapeLayer.setAffineTransform(mirrorUpsideDown)
shapeLayer.frame = shapeFrame

Trail of nodes along a path as in hello world

hi I have a node traveling back and forth along a path. I am trying to integrate the moving shapeNode as in the hello world example to trail my moving node. I'm using a trigger from frame update to trigger the copying of the shape node. And using the position of the travelling node.
The problem is that the trail has some sort of offset and i don't know where its coming from. I have tried to compensate but to no avail. I'm wondering if it might have anything to do with the actions. Thanks for reading.
I have tried looking at this link but i cannot translate
Here is my code so far
spriteKit xcode 9 swift 4 iPad Air
// GameScene.swift
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
var borderBody = SKPhysicsBody()
var myMovingNode = SKSpriteNode()
var trailNode : SKShapeNode?
override func didMove(to view: SKView) {
//Define Border
borderBody = SKPhysicsBody(edgeLoopFrom: self.frame)
borderBody.friction = 0
self.physicsBody = borderBody
physicsWorld.gravity = CGVector(dx: 0.0, dy: 0.0)
physicsWorld.contactDelegate = self
//Define myMovingNode
myMovingNode = SKSpriteNode(imageNamed: "greenball")
myMovingNode.size.width = 50
myMovingNode.size.height = 50
let myMovingNodeBody:CGSize = CGSize(width: 50, height: 50)
myMovingNode.physicsBody = SKPhysicsBody.init(rectangleOf: myMovingNodeBody)
myMovingNode.zPosition = 2
addChild(myMovingNode)
//Make Path for myMovingNode to travel
let myPath = CGMutablePath()
myPath.move(to: CGPoint(x: 30, y: 30))
myPath.addLine(to: CGPoint(x: 500, y: 500))
/*This is another path to try*/
// myPath.addCurve(to: CGPoint(x: 800, y: 700), control1: CGPoint(x: 500, y: 30), control2: CGPoint(x: 50, y: 500))
/*This draws a line along myPath*/
let myLine = SKShapeNode(path: myPath)
myLine.lineWidth = 10
myLine.strokeColor = .green
myLine.glowWidth = 0.5
myLine.zPosition = 2
addChild(myLine)
/*This sets myMovingNode running along myPath*/
let actionForward = SKAction.follow(myPath, asOffset: false, orientToPath: true, duration: 10)
let actionReverse = actionForward.reversed()
let wait = SKAction.wait(forDuration: 1)
let actionSequence = SKAction.sequence([actionForward, wait, actionReverse, wait])
myMovingNode.run(SKAction.repeatForever(actionSequence))
/*This defines TrailNode and its actions*/
trailNode = SKShapeNode.init(rectOf: CGSize.init(width: 20, height: 20), cornerRadius: 10)
if let trailNode = trailNode {
trailNode.lineWidth = 1
trailNode.fillColor = .cyan
trailNode.run(SKAction.sequence([SKAction.wait(forDuration: 5),
SKAction.fadeOut(withDuration: 3),
SKAction.removeFromParent()]))
}//Eo if Let
}//eo overdrive
func timeFunction (){/*This is controlled by func update*/
let n = trailNode?.copy() as! SKShapeNode?
/*this is where i'm trying to compensate*/
let movingNodeX = myMovingNode.position.x
let movingNodeY = myMovingNode.position.y
let movingNodeOffSet = CGPoint(x: movingNodeX - 0, y: movingNodeY - 0)
n?.position = movingNodeOffSet
myMovingNode.addChild(n!)
}
var frameCounter = 0
override func update(_ currentTime: TimeInterval) {
// print( "Called before each frame is rendered")
if frameCounter == 10{frameCounter = 0; timeFunction()}
frameCounter = frameCounter + 1
}
}//class GameScene
hi in answer to my own question. It was the simplest. On the last line change from
myMovingNode.addChild(n!) to addChild(n!)

Assign Multiple Masks to SKCropNode

As an example, I have a circle which travels to the left and then the right on the screen. I want the circle to only be visible if it is inside two specific squares (maskNodes). I'm using SKCropNode to try and achieve this, but the SKCropNode mask only lets me assign one mask. Does anybody know a way of assigning two or more mask to a SKCropNode, or if it is even possible to do so. Thanks!
override func didMoveToView(view: SKView) {
anchorPoint = CGPointMake(0.5, 0.5)
backgroundColor = UIColor.whiteColor()
let mask1 = SKSpriteNode(color: UIColor.blackColor(), size: CGSizeMake(50, 50))
mask1.position.x = -100
let mask2 = SKSpriteNode(color: UIColor.blackColor(), size: CGSizeMake(50, 50))
mask2.position.x = 100
let cropNode = SKCropNode()
cropNode.maskNode = mask1 // && mask2
addChild(cropNode)
let circle = SKShapeNode(circleOfRadius: 25)
circle.fillColor = UIColor.blackColor()
cropNode.addChild(circle)
// Move Circle
let moveLeft = SKAction.moveToX(-frame.size.width/2, duration: 2)
let moveRight = SKAction.moveToX(frame.size.width/2, duration: 2)
let seq = SKAction.repeatActionForever(SKAction.sequence([moveLeft, moveRight]))
circle.runAction(seq)
}
Figured it out, just has to add the two mask to a parent and then assign the parent as the SKCropNode mask.
override func didMoveToView(view: SKView) {
anchorPoint = CGPointMake(0.5, 0.5)
backgroundColor = UIColor.whiteColor()
let maskParent = SKSpriteNode()
let mask1 = SKSpriteNode(color: UIColor.blackColor(), size: CGSizeMake(50, 50))
mask1.position.x = -100
maskParent.addChild(mask1)
let mask2 = SKSpriteNode(color: UIColor.blackColor(), size: CGSizeMake(50, 50))
mask2.position.x = 100
maskParent.addChild(mask2)
let cropNode = SKCropNode()
cropNode.maskNode = maskParent
addChild(cropNode)
let circle = SKShapeNode(circleOfRadius: 25)
circle.fillColor = UIColor.blackColor()
cropNode.addChild(circle)
// Move Circle
let moveLeft = SKAction.moveToX(-frame.size.width/2, duration: 2)
let moveRight = SKAction.moveToX(frame.size.width/2, duration: 2)
let seq = SKAction.repeatActionForever(SKAction.sequence([moveLeft, moveRight]))
circle.runAction(seq)
}