How to scale 3D object vertically (along y-axis) in Swift? - ios

I created a door with ARKit and I want create a scale animation. My goal is to scale it only along the y-axis (stretch the door to be longer).
I want the door to grow within a duration of 1 second.
My approach was to simply scale it but I only have options that allow me to scale my entire object along all 3 axis.
Next I tried node.scale = SCNVector3(0, 2, 0) and that works ok, but it has no nice animation to it. When I create a SCNAction() and run the code as a block with duration time, it still just changes the size without any smooth animation.

You need to use SCNTransaction. The simplest way to animate your node scaling would be something like this:
SCNTransaction.begin()
SCNTransaction.animationDuration = 3
node.scale = SCNVector3(0, 2, 0)
SCNTransaction.commit()
You might also need to modify pivot property of the node to position the animation correctly.

Related

Center a generated Row in a SpriteKit Scene

I am making a Breakout Game and the Game generates rows from how ever many bricks I want in that row. But I cannot get the rows to start in the center. How would I achieve this, here is my current code.
I've tried numerous things, but the closest I got was to get them all to be in the middle, but the should spread out from the middle.
What I would like to achieve should look something like this.
If you have the maximum row size available, a simple way is to include an offset when you calculate the x coordinate.
let maxRow = ... // whatever the maximum row size is
let padding = (maxRow - row) / 2 // how many blocks to skip on the left
for i in 1...row {
// your block creation loop
let x = 15 + (i + padding) * Int(brick.size.width)
// make the brick at x, y
}
If things might not divide evenly (e.g., if maxRow is 15 like it seems to be in your pictures, but row could be 4) then you have to decide on what behavior you'd like. The above will keep bricks aligned because the integer division in calculating padding will truncate; the left and right padding won't be the same. If you use floating division and get a padding like 3.5, you'd get perfectly centered rows but odd rows and even rows would have different alignment.
If your problem is that you want things to appear in an animated way (like in your picture), then things are more complicated. Instead of generating based on the sequence 1, 2, ..., row, it's probably easier to think about generating from the sequence 0, +1, -1, +2, -2, ... After generating the center brick, you'd do the generation of additional bricks in pairs. The x coordinates would be calculated as xCenter + i * Int(brick.side.width).
You're adding the bricks into a parent SKNode (using addChild(_:)). Everything added in that parent node is relative to its coordinate space.
Try adding a brick at y: 0 and see where will it be positioned. If your scene has the anchorPoint at 0.5, you will likely see the brick in the center of the screen. From there, you go up or down (positive or negative y values) to show other rows.
To better understand how the coordinate system works, see About SpriteKit Coordinate Systems.
It's the same for the x axis. If your bricks start from left to right, that means the parent node has its origin to the left of the screen.
To start from the middle, you need to know the middle point. One option is to make a container node, position that in the center of the screen, and add all the bricks there. Like that, the bricks can start at (0, 0).
let middle = CGPoint(x: scene!.size.width / 2, y: scene!.size.height / 2)
let container = SKNode()
container.position = middle
// inside makeBricks(), add everything inside container:
container.addChild(brick)
It really depends on how you set up your node hierarchy.

Rotating around anchor point in SceneKit

Objective: There is a SCNCylinder object in my scene. I want to be able to drag one end of the cylinder and rotate the cylinder in any direction, while keeping the other end in the same position.
Currently, I am calling localRotate(by: SCNQuaternion) to first rotate the node, then calculate the position offset needed to move the cylinder so that the other end can go back to its original position.
How can I achieve the objective in one step instead of what I am doing now?
The pivot property is what you're looking for. Or, since modern SceneKit often works better / makes nicer Swift / interoperates easier with ARKit when you use SIMD types, the simdPivot property.
Note this bit in the docs:
Changing the pivot transform alters these behaviors in many useful ways. You can:
Offset the node’s contents relative to its position. For example, by setting the pivot to a translation transform you can position a node containing a sphere geometry relative to where the sphere would rest on a floor instead of relative to its center.
Move the node’s axis of rotation. For example, with a translation transform you can cause a node to revolve around a faraway point instead of rotating around its center, and with a rotation transform you can tilt the axis of rotation.
Similarly, for a cylinder, you can make its pivot a transform matrix that translates the origin by half its height, giving it an "anchor point" (for position and rotation changes) at one end instead of in the center. Something like this (untested):
let cylinder = SCNCylinder(radius: /*...*/, height: /*...*/)
let cylinderNode = SCNNode(geometry: cylinder)
cylinderNode.simdPivot = float4x4(translation: cylinder.height / 2)
extension float4x4 {
init(translation vector: float3) {
self.init(float4(1, 0, 0, 0),
float4(0, 1, 0, 0),
float4(0, 0, 1, 0),
float4(vector.x, vector.y, vector.z, 1))
}
}
More generally, whenever you're using a scene-graph / transform-hierarchy based graphics framework, any time you find yourself doing math depending on one transform (rotation, translation, etc) to affect another, it's always good to check for API that can do that math for you — because doing that kind of math is what transform hierarchy is all about.
And if there's not an API fairly specific to what you need, remember that the hierarchy itself is good for making dependent transforms. For example, if you want one node to follow a circular orbit around another, you don't need to set its position using sines and cosines... just make it the child of another node, and rotate that other node.
In this case, pivot is a convenience equivalent to using the node hierarchy. You could just as well create an intermediate node and move the cylinder within it (something like this):
let cylinder = SCNCylinder(radius: /*...*/, height: /*...*/)
let cylinderNode = SCNNode(geometry: cylinder)
let offsetNode = SCNNode()
offsetNode.addChildNode(cylinderNode)
cylinderNode.simdPosition.y = cylinder.height / 2
offsetNode.position = /*...*/ // set world-space position of end of cylinder
offsetNode.eulerAngles.x = /*...*/ // rotate cylinder around its end

Snake game movement issue Spritekit

I am trying to create a basic snake game using Swift and Spritekit. I have a sprite moving on the screen and when i swipe it starts moving in the swipe direction. I do this in the update method where I change the position of the sprite based on the swipe direction, a set speed and a fixed duration.
e.g. direction.x * blockMovePerSecond * CGFloat(actionDuration)
I have the sprites following each other, however, as soon as i swipe and the first sprite changes direction, the one following it moves diagonally instead of first on the x-axis and then the y-axis like a normal snakes game.
I tried the following options:
Tried keeping the distance that the sprite moves on each update equal
to the distance between the two sprites. However, the sprite runs off
the screen when i do that.
Made the first sprite create a path for the rest of the sprites to follow. However, the second sprite
runs off the screen and never shows up again. I think the problem is
because the path is open.
Here's my move method code for the sprites following the first sprite (snake head):
{
var temp:NSValue = lastNodeLocation // the lastnodelocation is from first sprite location
var tempNodeDirection = lastNodeDirection
let actionDuration = 1.0
let distanceToMoveThisFrame = tempNodeDirection.CGPointValue() * blockMovePerSecond * CGFloat(actionDuration)
var currentPosition = NSValue(CGPoint: blocksOfAlphabets[i].position)
let beforeDistance = CGPoint(x: temp.CGPointValue().x - currentPosition.CGPointValue().x, y: temp.CGPointValue().y - currentPosition.CGPointValue().y)
lastNodeLocation = NSValue(CGPoint: blocksOfAlphabets[i].position)
// move node to new location
var moveAction = SKAction.moveTo(temp.CGPointValue(), duration: 1.0)
node.runAction(moveAction)
}
Can somebody please help?
Thanks
Shuchi
Well your problem is that the runAction-methods don't wait to complete. So if you call another runAction-method while a sprite is already running an action, the new action will be started immediately.
You can work with sequences to finish the x-axis first: SKAction:sequence
I think the easiest thing to do here is to keep an array of snake sprite components (let's call these sprites a "Rib").
Every 5 seconds add a new Rib to the end of the array to make the snake grow.
Then, each half second in the update() method (or however long you want it to be):
check for any swipes that have happened since the last update and change direction of the first rib (the snake head) in the direction of the swipe.
now run through the snake array in reverse and set the location of node (n) to the node position of node (n-1).
There are other ways you could do it but I think this would be the easiest.

CATransform3D - understanding the transform values

The picture shows a simple UIView after applying the following transform:
- (CATransform3D) transformForOpenedMenu
{
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 /450;
transform = CATransform3DRotate(transform, D2R(40), 0, 1, 0);
transform = CATransform3DTranslate(transform, 210, 150, -500);
return transform;
}
I'm trying to make the distances highlighted with black to have equal length. Could you please help me understand the logic behind the values and calculations?
Cheers
UPD Sept 13
Looks like removing 3DTranslate keeps distances equal. I see I can use layer's frame property to reposition rotated view to the bottom left of the screen. Not yet sure, but this might actually work.
The .m34 value you are setting is best set on the sublayerTransform of the containing view rather than the view you are transforming.
I don't fully understand the maths behind affine transforms so I made this project which allows me to play around with the transform values to achieve the effect I want. You can plug in the values from your code above and see what it looks like, though note that there is already a perspective value applied using the sublayerTransform property mentioned above.
For your specific case, I think you want to adjust the anchor point of the layer to (0.0,0.5) and apply the rotation transform only. This assumes you want the menu to swing back like a door, with the hinges on the left edge.
The problem you're seeing is caused by your CATransform3DTranslate call. You're essentially setting the Y Axis off center, and hence seeing a different perspective view of the frame.
Think of it this way;
You're standing in the center of a long narrow field stretching off into the horizon. The edge of the field appears as if it is converges to a center point somewhere off in the distance. The angle of each edge to the converging point will appear equal if you are at the center of the field. If, on the other hand, you move either to the left or the right, the angles change and one will seem greater than the other (inversely opposite of course).
This is essentially what is happening with your view; As your converging points are to the right, changing the Y axis away from 0 will have the same effect as moving to the left or right in my example above. You're no longer looking at the parallel lines from the center.
so in your code above Setting the ty in CATransform3DTranslate to 0 Should fix your problem I.E.
transform = CATransform3DTranslate(transform, 210, 0, -500);
You may also need to alter the tz and tx Value to make it fit.
OK, so what eventually solved my question is this:
3D transform on Y axis to swing the view like a door transform = CATransform3DRotate(transform, D2R(40), 0, 1, 0);
set Z anchor point on a layer, to move it back targetView.layer.anchorPointZ = 850;
adjust layer position so that the view is located slightly to the bottom left of the parent view:
newPosition.x += 135 * positionDirection;
newPosition.y += 70 * positionDirection;
This sequence adjusts position without CATransform3DTranslate and keeps the 'swinged' effect not distorted.
Thanks everybody!

CAKeyframeAnimation - animate along segment of a path

I'm trying to animate an object along a specific segment of a path using CAKeyframeAnimation. Is this possible?
Think of a path as parameterized between 0 and 1, which is what an animation will typically run on (from the start of the curve to the end of the curve). Is there a way I can run an animation, say, from .25 to .75 of this path?
I know I can offset an animation with the 'offset' parameter, but this only adjusts the start position and still loops around to the end of the curve. It also doesn't correctly obey the CAMediaTiming function, if set.

Resources