How do I draw a line in SpriteKit with Swift 3? - swift

For one of my classes I made a javascript game that involve characters that jump to random parts of the screen. I am trying to convert this game to Swift (I am very bad at coding and this is my third day working with Swift).
I got the part where the characters (SKSpriteNode) bounce around the screen. But for some reason I have really struggled with drawing the lines. I tried using UIBezierPath() & CAShapeLayer(). I also tried an SKSpriteNode.
But I haven't been able to draw a line at the proper angle between the original point and the ending point of the characters. Because the characters move to random spots the angle changes all the time and the size of the line also needs to change.
Here's a picture of the Javascript game:

You can use something like this:
var line = SKShapeNode()
func drawLine(from: CGPoint, to: CGPoint){
line.removeFromParent()
let path = CGMutablePath()
path.move(to: from)
path.addLine(to: to)
line = SKShapeNode(path: path)
self.addChild(line)
}
override func update(_ currentTime: TimeInterval) {
drawLine(from: player.position, to: enemy.position)
}
Here you create a path and add a startpoint to it via path.move(to:). Then you add a line to your other point with path.addLine(to:). Then use the SKShapeNode(path: CGPath) initializer to create a SKShapeNode from the path. The SKShapeNode will present your line in the scene. At the end just add your node to the scene via addChild() and call this function in your update() function to recreate the line when the player or the enemy is moving.

Related

Scenekit / Spritekit remove node causing crash

What I'm try to do, is replicate some digital instrument gauge ...using Spritekit..
On my project I have to use a SpriteKit Scene as material for Scenekit SCNnode.
In order to do that I create a sub class of SKscene and and apply it as "material.diffuse.contents" of the SCNNode.
all working fine, I can see my SKscene as material of the SCNNode.
Here the issue:
I try to animate/replicate the correct indication of the instrument based of some value my app received from other source.
To do so, i decide to use the SKsceneDelegate and use the method "update(_ currentTime: TimeInterval)"
simply the update method look for the sknode of the green needle "fulcro" remove it , and make a new line calculating the angle and new coordinate for the new line.
override func update(_ currentTime: TimeInterval) {
guard let fulcroToRemove = self.childNode(withName: "fulcro") else {return}
fulcroToRemove.removeFromParent() // issue here...
let fulcro = SKNode()
fulcro.name = "fulcro"
fulcro.position = CGPoint(x: 300, y: 260)
let line = makeLine(startPos: CGPoint(x: 0, y: 0),
endPos: getCoordinate(angleDeg: getCurrentEGT()),
name: "EGT_LINE")
fulcro.addChild(line)
self.addChild(fulcro)
}
... if I try this SKscene itself in a project only with Spritekit all working fine.. I can see my needle increase the value correctly when the data source increase.
But if use the same SKscene as material of SceneKit getting the following error:
look like I'm removing something not correctly .. and I can see fulcroToRemove.removeFromParent() is the line trigger the error.
But why ??How I can remove the line to avoid the crash?

How to apply these transformations to a line in SpriteKit

I'm not sure what tools I should use for what I'm trying to do since I'm only really familiar with SKSpriteNodes and a little bit with SKShapeNodes.
My mission is as follows:
Add a line to the scene, SKShapeNode?
Rotate the line along it's bottom point (beginning point?) by some angle. Imagine a clock hand for this, rotating around the bottom point
Find the new point (x,y coord) of the top point (end point?) after the line has been translated
Does anyone know how I can accomplish this? I'm currently using an SKShapeNode for my line and rotating it with .zRotation but I can't seem to accomplish my goal. There doesn't seem to be an achorPoint property for SKShapeNodes, so I can't change the point of rotation. Also I'm clueless on how to find the position of the end point of my line AFTER it has been rotated, I created it as follows:
let linePath = CGMutablePath()
linePath.move(to: begin)
linePath.addLine(to: end)
let line = SKShapeNode()
line.path = linePath
line.strokeColor = UIColor.black
line.lineWidth = 5
SceneCoordinator.shared.gameScene.addChild(line)
I'm rotating using:
public func rotate(angle: Double) {
var transform = CGAffineTransform(rotationAngle: CGFloat(angle))
line.path = linePath.mutableCopy(using: &transform)
}
SKShapeNode can be pretty expensive if you notice your FPS dropping. Also, you can easily turn a shape into a sprite to get .anchorPoint, but be warned that anchorpoint's behavior is not always as expected and you may have bugs later on (especially with physics):
func shapeToSprite(_ shape: SKShapeNode) -> SKSpriteNode {
let sprite = SKSpriteNode(texture: SKView().texture(from: shape))
sprite.physicsBody = shape.physicsBody // Or create a new PB from alpha mask (may be slower, IDK)
shape.physicsBody = nil
return sprite
}
override func didMove(to view: SKView) {
let shape = SKShapeNode(circleOfRadius: 60)
let sprite = shapeToSprite(shape)
sprite.anchorPoint = CGPoint()
addChild(sprite)
}
Otherwise, you are going to have to either 1), redraw the line with the correct rotation, 2), rotate the shape then reposition it at a new location...
Both are going to be MATH :[ so hopefully the anchorppoint works for you

How do I programmatically move an ARAnchor?

I'm trying out the new ARKit to replace another similar solution I have. It's pretty great! But I can't seem to figure out how to move an ARAnchor programmatically. I want to slowly move the anchor to the left of the user.
Creating the anchor to be 2 meters in front of the user:
var translation = matrix_identity_float4x4
translation.columns.3.z = -2.0
let transform = simd_mul(currentFrame.camera.transform, translation)
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
later, moving the object to the left/right of the user (x-axis)...
anchor.transform.columns.3.x = anchor.transform.columns.3.x + 0.1
repeated every 50 milliseconds (or whatever).
The above does not work because transform is a get-only property.
I need a way to change the position of an AR object in space relative to the user in a way that keeps the AR experience intact - meaning, if you move your device, the AR object will be moving but also won't be "stuck" to the camera like it's simply painted on, but moves like you would see a person move while you were walking by - they are moving and you are moving and it looks natural.
Please note the scope of this question relates only to how to move an object in space in relation to the user (ARAnchor), not in relation to a plane (ARPlaneAnchor) or to another detected surface (ARHitTestResult).
Thanks!
You don't need to move anchors. (hand wave) That's not the API you're looking for...
Adding ARAnchor objects to a session is effectively about "labeling" a point in real-world space so that you can refer to it later. The point (1,1,1) (for example) is always the point (1,1,1) — you can't move it someplace else because then it's not the point (1,1,1) anymore.
To make a 2D analogy: anchors are reference points sort of like the bounds of a view. The system (or another piece of your code) tells the view where it's boundaries are, and the view draws its content relative to those boundaries. Anchors in AR give you reference points you can use for drawing content in 3D.
What you're asking is really about moving (and animating the movement of) virtual content between two points. And ARKit itself really isn't about displaying or animating virtual content — there are plenty of great graphics engines out there, so ARKit doesn't need to reinvent that wheel. What ARKit does is provide a real-world frame of reference for you to display or animate content using an existing graphics technology like SceneKit or SpriteKit (or Unity or Unreal, or a custom engine built with Metal or GL).
Since you mentioned trying to do this with SpriteKit... beware, it gets messy. SpriteKit is a 2D engine, and while ARSKView provides some ways to shoehorn a third dimension in there, those ways have their limits.
ARSKView automatically updates the xScale, yScale, and zRotation of each sprite associated with an ARAnchor, providing the illusion of 3D perspective. But that applies only to nodes attached to anchors, and as noted above, anchors are static.
You can, however, add other nodes to your scene, and use those same properties to make those nodes match the ARSKView-managed nodes. Here's some code you can add/replace in the ARKit/SpriteKit Xcode template project to do that. We'll start with some basic logic to run a bouncing animation on the third tap (after using the first two taps to place anchors).
var anchors: [ARAnchor] = []
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Start bouncing on touch after placing 2 anchors (don't allow more)
if anchors.count > 1 {
startBouncing(time: 1)
return
}
// Create anchor using the camera's current position
guard let sceneView = self.view as? ARSKView else { return }
if let currentFrame = sceneView.session.currentFrame {
// Create a transform with a translation of 30 cm in front of the camera
var translation = matrix_identity_float4x4
translation.columns.3.z = -0.3
let transform = simd_mul(currentFrame.camera.transform, translation)
// Add a new anchor to the session
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
anchors.append(anchor)
}
}
Then, some SpriteKit fun for making that animation happen:
var ballNode: SKLabelNode = {
let labelNode = SKLabelNode(text: "🏀")
labelNode.horizontalAlignmentMode = .center
labelNode.verticalAlignmentMode = .center
return labelNode
}()
func startBouncing(time: TimeInterval) {
guard
let sceneView = self.view as? ARSKView,
let first = anchors.first, let start = sceneView.node(for: first),
let last = anchors.last, let end = sceneView.node(for: last)
else { return }
if ballNode.parent == nil {
addChild(ballNode)
}
ballNode.setScale(start.xScale)
ballNode.zRotation = start.zRotation
ballNode.position = start.position
let scale = SKAction.scale(to: end.xScale, duration: time)
let rotate = SKAction.rotate(toAngle: end.zRotation, duration: time)
let move = SKAction.move(to: end.position, duration: time)
let scaleBack = SKAction.scale(to: start.xScale, duration: time)
let rotateBack = SKAction.rotate(toAngle: start.zRotation, duration: time)
let moveBack = SKAction.move(to: start.position, duration: time)
let action = SKAction.repeatForever(.sequence([
.group([scale, rotate, move]),
.group([scaleBack, rotateBack, moveBack])
]))
ballNode.removeAllActions()
ballNode.run(action)
}
Here's a video so you can see this code in action. You'll notice that the illusion only works as long as you don't move the camera — not so great for AR. When using SKAction, we can't adjust the start/end states of the animation while animating, so the ball keeps bouncing back and forth between its original (screen-space) positions/rotations/scales.
You could do better by animating the ball directly, but it's a lot of work. You'd need to, on every frame (or every view(_:didUpdate:for:) delegate callback):
Save off the updated position, rotation, and scale values for the anchor-based nodes at each end of the animation. You'll need to do this twice per didUpdate callback, because you'll get one callback for each node.
Work out position, rotation, and scale values for the node being animated, by interpolating between the two endpoint values based on the current time.
Set the new attributes on the node. (Or maybe animate it to those attributes over a very short duration, so it doesn't jump too much in one frame?)
That's kind of a lot of work to shoehorn a fake 3D illusion into a 2D graphics toolkit — hence my comments about SpriteKit not being a great first step into ARKit.
If you want 3D positioning and animation for your AR overlays, it's a lot easier to use a 3D graphics toolkit. Here's a repeat of the previous example, but using SceneKit instead. Start with the ARKit/SceneKit Xcode template, take the spaceship out, and paste the same touchesBegan function from above into the ViewController. (Change the as ARSKView casts to as ARSCNView, too.)
Then, some quick code for placing 2D billboarded sprites, matching via SceneKit the behavior of the ARKit/SpriteKit template:
// in global scope
func makeBillboardNode(image: UIImage) -> SCNNode {
let plane = SCNPlane(width: 0.1, height: 0.1)
plane.firstMaterial!.diffuse.contents = image
let node = SCNNode(geometry: plane)
node.constraints = [SCNBillboardConstraint()]
return node
}
// inside ViewController
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// emoji to image based on https://stackoverflow.com/a/41021662/957768
let billboard = makeBillboardNode(image: "⛹ī¸".image())
node.addChildNode(billboard)
}
Finally, adding the animation for the bouncing ball:
let ballNode = makeBillboardNode(image: "🏀".image())
func startBouncing(time: TimeInterval) {
guard
let sceneView = self.view as? ARSCNView,
let first = anchors.first, let start = sceneView.node(for: first),
let last = anchors.last, let end = sceneView.node(for: last)
else { return }
if ballNode.parent == nil {
sceneView.scene.rootNode.addChildNode(ballNode)
}
let animation = CABasicAnimation(keyPath: #keyPath(SCNNode.transform))
animation.fromValue = start.transform
animation.toValue = end.transform
animation.duration = time
animation.autoreverses = true
animation.repeatCount = .infinity
ballNode.removeAllAnimations()
ballNode.addAnimation(animation, forKey: nil)
}
This time the animation code is a lot shorter than the SpriteKit version.
Here's how it looks in action.
Because we're working in 3D to start with, we're actually animating between two 3D positions — unlike in the SpriteKit version, the animation stays where it's supposed to. (And without the extra work for directly interpolating and animating attributes.)

Swift - SpriteKit CGPoint Alignment

I'm working on a project in swift and I am able to make a sprite. I am trying to make a sprite at a number of different locations. As a test I replaced the code in the game scene to be:
class GameScene: SKScene {
override func didMoveToView(view: SKView){
let wall = SKSpriteNode(imageNamed: "Wall")
wall.position = CGPoint(x: 289, y: 0)
}
}
What I do not understand is this code makes a sprite in the lower left corner. As expected in the y direction only half of the sprite shows, but the x direction seems completely off. If I try to set the x value to any number below 289 it will not appear. Is the 0,0 point really 289,0 or am I missing something? I am programming for iphone 6 if that makes a difference.
Thank you!
Your scene it is not necessarily the same size of your view. You can check the size of your scene as follow:
view.scene!.frame.size
view.scene!.frame.width
view.scene!.frame.height
view.scene!.frame.midX
view.scene!.frame.midY

Constraining movement of a SKSpriteNode to a fixed area within a scene

How can I constrain manual movement of an SKSpriteNode to a fixed rectangular area within a scene? This fixed rectangular area a also a SKSpriteNode which is fixed within the scene. In other words, I want to constrain manual movement of an object (SKSpriteNode) to be completely contained within another SKSpriteNode or at least in the same space that it occupies. I have tried several different approaches (e.g. using an SKShapeNode that has an edged-based physics body), but nothing seems to work. This seems like it should be a fairly simple task to accomplish. Thanks for any help or hints you can offer.
Put an if statement around your moving code - so don't carry out the movement if it will take the object past your boundary. e.g.
//check that a positive movement won't take your node past the right boundary
if(node.position.x + yourXMovementValue < boundaryXRight){
//move your node
}
//same for y
let rangeX = SKRange(lowerLimit: CGFloat, upperLimit: CGFloat)
let contraintX = SKConstraint.positionX(rangeX)
let rangeY = SKRange(lowerLimit: CGFloat, upperLimit: CGFloat)
let contraintY = SKConstraint.positionY(rangeY)
yourObject.constraints = [contraintX, contraintY]