SpriteKit weird behaviour of SKSpriteNode property - swift

I'm moving my first steps into SpriteKt and found this apparently weird behaviour of the SkSpriteNode.position property: it should be a CGPoint, but when assigned and read back, it behaves differently of any other CGPoint.
In the code that follows I have tested some randomly chosen points. Some of them can be assigned to the SkSpriteNode.position property and read back again and everything seems to work fine.
Some other, when assigned, are rounded to some other (close) value.
It could be a floating-point value representation round off issue, but than comes the super weird thing: the exact same CGPoint, when assigned to another CGPoint (not to the position property, which, by the way, claims to be a CGPoint) the round off doesn't appear 😳
Does anybody knows what's going on?
check(position: CGPoint.zero)
check(position: CGPoint(x: 1, y: 1))
check(position: CGPoint(x: 100, y: 700))
check(position: CGPoint(x: -500, y: 200))
check(position: CGPoint(x: 0.5, y: 0.5))
check(position: CGPoint(x: 1.12, y: 1.14))
check(position: CGPoint(x: 1.12, y: 1.1415161718))
func check(position: CGPoint) {
let sprite = SKSpriteNode(color: .cyan, size: CGSize(width: 32, height: 32))
let point = position
sprite.position = position
print("position = \(position)")
print("point = \(point)")
print("sprite = \(sprite.position)")
if sprite.position == position {
print("✅ they match")
} else {
print("❌ they don't match")
}
assert(position == point, "assign \(position) to point result in \(point)")
}
On my system (Xcode Version 12.5 (12E262)) output is:
position = (0.0, 0.0)
point = (0.0, 0.0)
sprite = (0.0, 0.0)
✅ they match
position = (1.0, 1.0)
point = (1.0, 1.0)
sprite = (1.0, 1.0)
✅ they match
position = (100.0, 700.0)
point = (100.0, 700.0)
sprite = (100.0, 700.0)
✅ they match
position = (-500.0, 200.0)
point = (-500.0, 200.0)
sprite = (-500.0, 200.0)
✅ they match
position = (0.5, 0.5)
point = (0.5, 0.5)
sprite = (0.5, 0.5)
✅ they match
position = (1.12, 1.14)
point = (1.12, 1.14)
sprite = (1.1200000047683716, 1.1399999856948853)
❌ they don't match
position = (1.12, 1.1415161718)
point = (1.12, 1.1415161718)
sprite = (1.1200000047683716, 1.1415162086486816)
❌ they don't match

Internally the sprite is using 32-bit single-precision floats. The normal CGPoint is 64-bit double-precision. Assigning sprite.position is rounding 64-bit to 32-bit, and then for the printing and comparison it's going back from 32-bit to 64-bit. Your initial examples all happen to be numbers that are exactly representable in both 32- and 64-bits, so no loss of precision is happening there.

Related

SKAction.move(to: ...) and SKAction.move(by: ...) does the exact same thing for me

I've set up an SKScene, that I fill with a bunch of blocks. Upon touch I want to move a random block to the 0,0 position of the SKScene. 0,0 is not the final goal, but I use it for test. My node is called a bixel and I naturally thought that using SKAction.move(to: ...) would move it to an absolute coordinate, as stated in the documentation, however I must be missing something, as moving to 0,0 does nothing, however moving to say 100,0 moves it 100 pixels to the right.
I've tried all the move(to: ...) (moveTo(x: ...) etc. And I've tried move(by: ...) which works as it should.
With the code provided below, if I switch between action and action2 (by rebuilding the project), the outcome is always the same, it's moved 100 pixels to the right.
I've also tried the point convert:
let point = self.convert(CGPoint(x: 0, y: 0), to: bixel)
but the result is the same.
let bixel = bixels[Int.random(in: 0..<bixels)] bixel.fillColor = .red
let action = SKAction.move(to: CGPoint(x: 100, y: 0), duration: 0.1)
let action2 = SKAction.move(by: CGVector(dx: 100, dy: 0), duration: 0.1)
bixel.zPosition = 1000
bixel.run(action)
EDIT: You are completely correct #Martin R. And as I wrote in my comment to your answer every ShapeNode thought that their position was 0,0. It was because of how I initialised them.
I did this:
let bixel = SKShapeNode(
rect: CGRect(
x: xOffset + (xF * shapeSize.width),
y: self.size.height - yOffset - (yF * shapeSize.height),
width: shapeSize.width - space,
height: shapeSize.height - space
),
cornerRadius: 2
)
Which made it think it's position was 0,0. So when I just changed it to this:
let bixel = SKShapeNode(rectOf: shapeSize, cornerRadius: 2)
bixel.position = CGPoint(
x: xOffset + (xF * shapeSize.width),
y: self.size.height - yOffset - (yF * shapeSize.height)
)
everything worked. Thanks!
SKAction.move(to:duration:) creates an action that moves a node to the given position.
SKAction.move(to: CGPoint(x: 100, y: 0), duration: 0.1)
creates an action that moves a node to position (100, 0).
SKAction.move(by:duration:) creates an action that moves a node relative to its current position.
SKAction.move(by: CGVector(dx: 100, dy: 0), duration: 0.1)
creates an action that moves a node from its current position (x0, y0) to the new position (x0+100, y0).
Both actions have the same effect if (and only if) the current node position is (0, 0).

Create UIView with rounded specified borders

I want to create a UIView that has rounded borders which I have specified. For example, I want to create a UIView that has 3 borders: left, top and right, and the topright and topleft borders should be rounded.
This let's me create resizable border views (without rounded corners):
open class ResizableViewBorder: UIView {
open override func layoutSubviews() {
super.layoutSubviews()
setNeedsDisplay()
}
open override func draw(_ rect: CGRect) {
let edges = ... get some UIRectEdges here
let lineWidth = 1
if edges.contains(.top) || edges.contains(.all) {
addBezierPath(paths: [
CGPoint(x: 0, y: 0 + lineWidth / 2),
CGPoint(x: self.bounds.width, y: 0 + lineWidth / 2)
])
}
if edges.contains(.bottom) || edges.contains(.all) {
addBezierPath(paths: [
CGPoint(x: 0, y: self.bounds.height - lineWidth / 2),
CGPoint(x: self.bounds.width, y: self.bounds.height - lineWidth / 2)
])
}
if (edges.contains(.left) || edges.contains(.all) || edges.contains(.right)) && CurrentDevice.isRightToLeftLanguage{
addBezierPath(paths: [
CGPoint(x: 0 + lineWidth / 2, y: 0),
CGPoint(x: 0 + lineWidth / 2, y: self.bounds.height)
])
}
if (edges.contains(.right) || edges.contains(.all) || edges.contains(.left)) && CurrentDevice.isRightToLeftLanguage{
addBezierPath(paths: [
CGPoint(x: self.bounds.width - lineWidth / 2, y: 0),
CGPoint(x: self.bounds.width - lineWidth / 2, y: self.bounds.height)
])
}
}
private func addBezierPath(paths: [CGPoint]) {
let lineWidth = 1
let borderColor = UIColor.black
let path = UIBezierPath()
path.lineWidth = lineWidth
borderColor.setStroke()
UIColor.blue.setFill()
var didAddedFirstLine = false
for singlePath in paths {
if !didAddedFirstLine {
didAddedFirstLine = true
path.move(to: singlePath)
} else {
path.addLine(to: singlePath)
}
}
path.stroke()
}
}
However, I can not find a nice robust way to add a corner radius to a specified corner. I have a hacky way to do it with a curve:
let path = UIBezierPath()
path.lineWidth = 2
path.move(to: CGPoint(x: fakeCornerRadius, y: 0))
path.addLine(to: CGPoint(x: frame.width - fakeCornerRadius, y: 0))
path.addQuadCurve(to: CGPoint(x: frame.width, y: fakeCornerRadius), controlPoint: CGPoint(x: frame.width, y: 0))
path.addLine(to: CGPoint(x: frame.width, y: frame.height - fakeCornerRadius))
path.stroke()
Which gives me this:
Why is that line of the quad curve so fat? I prefer using UIBezierPaths over CALayers because CALayers have a huge performance impact...
What's wrong with your curve-drawing code is not that the curve is fat but that the straight lines are thin. They are thin because they are smack dab on the edge of the view. So your line width is 2 points, but one of those points is outside the view. And points are not pixels, so what pixels are there left to fill in? Only the ones inside the view. So the straight lines have an apparent visible line width of 1, and only the curve has a visible line width 2.
Another problem is that you probably are looking at this app running in the simulator on your computer. But there is a mismatch between the pixels of the simulator and the pixels of your computer monitor. That causes numerous drawing artifacts. The way to examine the drawing accurately down to the pixel level is to use the simulator application's screen shot image facility and look at the resulting image file, full-size, in Preview or similar. Or run on a device and take the screen shot image there.
To demonstrate this, I modified your code to operate in an inset version of the original rect (which, by the way, should be your view's bounds, not its frame):
let fakeCornerRadius : CGFloat = 20
let rect2 = self.bounds.insetBy(dx: 2, dy: 2)
let path = UIBezierPath()
path.lineWidth = 2
path.move(
to: CGPoint(x: rect2.minX + fakeCornerRadius, y: rect2.minY))
path.addLine(
to: CGPoint(x: rect2.maxX - fakeCornerRadius, y: rect2.minY))
path.addQuadCurve(
to: CGPoint(x: rect2.maxX, y: rect2.minY + fakeCornerRadius),
controlPoint: CGPoint(x: rect2.maxX, y: rect2.minY))
path.addLine(
to: CGPoint(x: rect2.maxX, y: rect2.maxY - fakeCornerRadius))
path.stroke()
Taking a screen shot from within the Simulator application, I got this:
As you can see, this lacks the artifacts of your screen shot.

Rotate SCNNode then move relative to rotation?

My node setup:
let node = SCNNode()
node.position = SCNVector3(0.0, 0.0, 0.0)
I then wish to rotate the node by 90 degrees to the left which can be achieved with:
node.transform = SCNMatrix4Rotate(node.transform, .pi/2, 0.0, 1.0, 0.0)
Next, I want to translate the node forward one unit along the negative z axis relative to current rotation and end up with:
node.position = SCNVector3(-1.0, 0.0, 0.0)
I have no idea how to get from the rotation of the node to the position of the node programmatically.
Basically node begins at (0, 0, 0), its forward vector is the -z axis, node turns left and moves one unit forward to end up at (-1, 0, 0).
This isn't working:
func move(_ direction: moveDirection) {
switch direction {
case .forward: characterNode.position = SCNVector3(characterNode.position.x, characterNode.position.y, characterNode.position.z - 1.0)
case .left: characterNode.pivot = SCNMatrix4Rotate(characterNode.pivot, -.pi/32, 0.0, 1.0, 0.0)
case .backward: characterNode.position = SCNVector3(characterNode.position.x, characterNode.position.y, characterNode.position.z + 1.0)
case .right: characterNode.pivot = SCNMatrix4Rotate(characterNode.pivot, .pi/32, 0.0, 1.0, 0.0)
}
}
Assuming I have understood you correctly, once you have rotated your SCNNode, you want to move it forward it in that direction.
This can be done by using the worldFront value which is simply:
The local unit -Z axis (0,0,-1) in world space.
As such this may be the answer you are looking for:
//1. Create A Node Holder
let nodeToAdd = SCNNode()
//2. Create An SCNBox Geometry
let nodeGeometry = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
nodeGeometry.firstMaterial?.diffuse.contents = UIColor.cyan
nodeGeometry.firstMaterial?.lightingModel = .constant
nodeToAdd.geometry = nodeGeometry
//3. Position It 1.5m Away From The Camera
nodeToAdd.position = SCNVector3(0, 0, -1.5)
//4. Rotate The Node By 90 Degrees On It's Y Axis
nodeToAdd.rotation = SCNVector4Make(0, 1, 0, .pi / 2)
//5. Add The Node To The ARSCNView
augmentedRealityView.scene.rootNode.addChildNode(nodeToAdd)
//6. Use The World Front To Move The Node Forward e.g
/*
nodeToAdd.simdPosition += nodeToAdd.simdWorldFront * 1.2
*/
nodeToAdd.simdPosition += nodeToAdd.simdWorldFront
//7. Print The Position
print(nodeToAdd.position)
/*
SCNVector3(x: -0.999999881, y: 0.0, z: -1.50000024)
*/

Move nodes by x and by y at the same time

I want to create a game with rectangles which move by x and by y.
My question is: Have I move the view (viewer of the player) or have I move the rectangles by x and by y at the same time?
I tried to move rectangles by x and by y at the same time, with the following code, but the rectangles move only by y.
#objc func addRects() {
rects = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: rects) as! [String]
rect = SKSpriteNode(imageNamed: rects[0])
rect.position = CGPoint(x: self.frame.width / 256 + 320, y: self.frame.height / 256)
rect.size = CGSize(width: 180, height: 120)
rect.zPosition = 1
self.addChild(rect)
moveByX = SKAction.moveTo(x: -500, duration: 2.0)
moveByY = SKAction.moveTo(y: -800, duration: 2.0)
removeRects = SKAction.removeFromParent()
wait = SKAction.wait(forDuration: 2.0)
rect.run(SKAction.sequence([moveByY,moveByY]))
rect.run(SKAction.sequence([wait,removeRects]))
}
How can I resolve this problem ?
Thank you
Don't use a sequence:
rect.run(moveByY)
rect.run(moveByX)
then it should move at the same time "moveByY" and "moveByX"
The other problem is, that you used:
rect.run(SKAction.sequence([moveByY,moveByY]))
I Hope my answer helps!

SCNKit: Scaling parent node not working

I'm trying to scale down a SCNNode that contains other nodes, but itself has no geometries. I read the documentation on scale, but I'm a little skeptical that they would make positioning relative to the parent, but not scale.
The problem:
scale does not seem to do anything.
Here's a snippet of my SCNNode sub-class
addChildNode(Node1)
addChildNode(Node2)
Node2.addChildNode(Node21)
addChildNode(Node3)
print("pre-scale", self.scale)
// prints SCNVector3(x: 1.0, y: 1.0, z: 1.0)
self.scale = SCNVector3(x:0.05, y:0.05, z:0.05)
print("post-scale", self.scale)
// prints SCNVector3(x: 0.05, y: 0.05, z: 0.05)
Visibly, nothing changes.
I've considered doing a loop and applying the scaling factor to every child node, but I think the relative positions will get all messed up.
I'd like everything to scale as one and retain its integrity. Is there something I'm missing?
Try to scale all child nodes one by one:
let newScale = Float(0.05)
for c in self.childNodes
{
c.scale = SCNVector3Make(newScale, newScale, newScale)
}
The parent node may not assign to any camera. try adding the camera to the node and try the scaling.
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
scene.rootNode.addChildNode(cameraNode)
parentNode.scale = SCNVector3(0.5, 0.5, 0.5)