I want to create the ability to spin a photographed object 360 degrees.
It spins endlessly based on the speed you "flick" .
You spin it left or right by flicking the object left or right .
You stop the spin when you touch to stop it if it's spinning.
Similar to the app The Elements by Theodore Grey.
Here's a video of the part of the app I'm trying to recreate. (i.e. the 3D spinner)
https://youtu.be/6T0hE0jGiYY
Here's a video of my finger interacting with it.
https://youtu.be/qjzeewpVN9o
I'm looking to use Swift and likely SpriteKit.
How can I get from a real life object to something high quality and
functional? I'm armed with a Mac , Nikon D810 and a green screen.
I.e I'm guessing that a series of stop motion pictures is the way to
go... but I'm feel that might not be fluid enough.
For the purposes of this question I want to figure out what would make the most sense to program with. E.g. a video I'm rewinding and fast forwarding on
command or a texture atlas of stop motion frames , etc.
Note: Capturing software and photography techniques would be helpful
information as I'm clueless in that department. But, I understand I
can ask that on https://photo.stackexchange.com/ .
What would the basic logic of my code be like for this object? In terms of:
A. The function setting up the object's animation or video or whatever is the best way to have the images prepared for use in my code.
B. The spin() function and
C. The stopSpin() function.
A whole project sample isn't needed (though I guess it'd be nice). But, those 3 functions would be enough to get me going.
Is SpriteKit the wisest choice?
Here is the second draft of my answer that shows off the basic functionality of a simple sprite animation:
class GameScene: SKScene {
// Left spin is ascending indices, right spin is descending indices.
var initialTextures = [SKTexture]()
// Reset then reload this from 0-6 with the correct image sequences from initialTextures:
var nextTextures = [SKTexture]()
var sprite = SKSpriteNode()
// Use gesture recognizer or other means to set how fast the spin should be.
var velocity = TimeInterval(0.1)
enum Direction { case left, right }
func spin(direction: Direction, timePerFrame: TimeInterval) {
nextTextures = []
for _ in 0...6 {
var index = initialTextures.index(of: sprite.texture!)
// Left is ascending, right is descending:
switch direction {
case .left:
if index == (initialTextures.count - 1) { index = 0 } else { index! += 1 }
case .right:
if index == 0 { index = (initialTextures.count - 1) } else { index! -= 1 }
}
let nextTexture = initialTextures[index!]
nextTextures.append(nextTexture)
sprite.texture = nextTexture
}
let action = SKAction.repeatForever(.animate(with: nextTextures, timePerFrame: timePerFrame))
sprite.run(action)
}
override func didMove(to view: SKView) {
removeAllChildren()
// Make our textures for spinning:
for i in 0...6 {
initialTextures.append(SKTexture(imageNamed: "img_\(i)"))
}
nextTextures = initialTextures
sprite.texture = nextTextures.first!
sprite.size = nextTextures.first!.size()
addChild(sprite)
spin(direction: .left, timePerFrame: 0.10)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
spin(direction: .right, timePerFrame: velocity)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
spin(direction: .left, timePerFrame: velocity)
}
}
Right now you just click / release to alternate right left.
Todo for next draft:
- Implement gesture recognizer for velocity
- Implement decay if wanted (so it will slow down over time)
(Old video, new code does not reset frame to 0):
Image assets are found here for the animation:
https://drive.google.com/open?id=0B3OoSBYuhlkgaGRtbERfbHVWb28
Related
I am working with SceneKit and ARKit. I have made a collectionView with an array of emoji's. Now I want the user to be able to select the emoji from collectionView and when he/she touches the screen that selected emoji will be placed in 3D.
How can I do that? I think I have to create a function for the Node, but still my idea is blurry in the mind and I am not very much clear.
As far as any emoji is a 2D element, it's better to use a SpriteKit framework to upload them, not a SceneKit. But, of course, you might choose a SceneKit as well. So, there are two ways you can work with emojis in ARKit:
Using SpriteKit. In that case all the 2D sprites you spawn in ARSKView are always face the camera. So, if the camera moves around a definite point of real scene, all the sprites are rotates about their pivot point facing a camera.
Using SceneKit. In ARSCNView you can use all your sprites as a texture for 3D geometry. This texture could be for a plane, cube, sphere, or any custom model, it's up to you. To make, for example, a plane (with emojis texture on it) to face a camera use SCNBillboardConstraint constraint.
Here's how you code in ViewController might look like:
// Element's index coming from `collectionView`
var i: Int = 0
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
let emojiArray = ["🐶","🦊","🐸","🐼","🐹"]
let emojiNode = SKLabelNode(text: emojiArray[i])
emojiNode.horizontalAlignmentMode = .center
emojiNode.verticalAlignmentMode = .center
return emojiNode
}
...and in Scene.swift file:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let sceneView = self.view as? ARSKView else { return }
if let currentFrame = sceneView.session.currentFrame {
var translation = matrix_identity_float4x4
translation.columns.3.z = -0.75 // 75 cm from camera
let transform = simd_mul(currentFrame.camera.transform, translation)
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
}
}
Or, if you use hit-testing, your code might look like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let sceneView = self.view as? ARSKView else { return }
if let touchLocation = touches.first?.location(in: sceneView) {
if let hit = sceneView.hitTest(touchLocation, types: .featurePoint).first {
sceneView.session.add(anchor: ARAnchor(transform: hit.worldTransform))
}
}
}
If you'd like to create an UICollectionView overlay containing emojis to choose from, read the following post.
If you'd like to create an SKView overlay, containing emojis to choose from, read the following post.
Do you know the puzzle game „voi“? That is a game which works with color-XOR-logic. That means: black + black = white.
https://www.youtube.com/watch?v=Aw5BdVcAtII
Is there any way to do the same color logic with two sprite nodes in sprit kit?
Thanks.
Of course, it's possible to do that in Sprite Kit.
Problem:
Let's say you have 2 black squares, squareA and squareB. The user can drag these two squares wherever he wants to. He can drag only one square at a time. You want to color the intersect area to white whenever the two squares intersect.
Initial Setup:
At the top of your scene, there are a few variables that we need to create:
private var squareA: SKSpriteNode?
private var squareB: SKSpriteNode?
private var squares = [SKSpriteNode]()
private var selectedShape: SKSpriteNode?
private var intersectionSquare: SKShapeNode?
squareA and squareB are just the 2 squares that we initially have on screen.
squares is an array and it will store all the squares that are showing on screen.
selectedShape will help us keeping track of the square that is currently being dragged.
intersectionSquare is a white square that represents the intersection area between the two black squares.
Then initialize squareA and squareB, and add them to the squares array like so:
squareA = SKSpriteNode(color: .black, size: CGSize(width: 190.0, height: 190.0))
if let squareA = self.squareA {
squareA.position = CGPoint(x: -200, y: 200)
squareA.name = "Square A"
squares.append(squareA)
self.addChild(squareA)
}
// Do the same for squareB or any other squares that you have on screen..
Note: As you can see, I gave it a name here just to make it easier to differentiate them during the testing phase.
Detect when user is dragging a square:
Now, you need to detect when the user is dragging a square. To do this, you can use:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
These are just helper methods that are going to make our life easier.
Then, you need to setup touchDown, touchMoved and touchUp methods:
func touchDown(atPoint pos : CGPoint) {
let touchedNode = self.nodes(at: pos)
guard let selectedSquare = touchedNode.first as? SKSpriteNode else {
return
}
selectedShape = selectedSquare
}
func touchMoved(toPoint pos : CGPoint) {
guard let selectedSquare = self.selectedShape else {
return
}
selectedSquare.position = pos
checkIntersectionsWith(selectedSquare)
}
func touchUp(atPoint pos : CGPoint) {
selectedShape = nil
}
To explain you in more details what is going on here:
In the touchDown method:
Well, we need the user to be able to drag only one square at a time. Using the nodes(at:) method, it's easy to know which square was touched, and we can know set our selectedShape variable to be equal to the square that was touched.
In the touchMoved method:
Here we are basically just moving the selectedShape to the position the user moves his finger at. We also call the checkIntersectionsWith() method that we will setup in a second.
In the touchUp method:
The user released his finger from the screen, so we can set the selectedShape to nil.
Change the color of the intersection frame:
Now the most important part to make your game actually look like the one you want to make, is how to change the color of the intersection frame to white when two black squares are intersecting ?
Well, you have different possibilities here, and here is one possible way of doing it:
private func checkIntersectionsWith(_ selectedSquare: SKSpriteNode) {
for square in squares {
if selectedSquare != square && square.intersects(selectedSquare) {
let intersectionFrame = square.frame.intersection(selectedSquare.frame)
intersectionSquare?.removeFromParent()
intersectionSquare = nil
intersectionSquare = SKShapeNode(rect: intersectionFrame)
guard let interSquare = self.intersectionSquare else {
return
}
interSquare.fillColor = .white
interSquare.strokeColor = .clear
self.addChild(interSquare)
} else if selectedSquare != square {
intersectionSquare?.removeFromParent()
intersectionSquare = nil
}
}
}
Every time the checkIntersectionsWith() method is called, we are iterating through the nodes that are inside our squares array, and we check, using the frame's intersection() method, if the selected square intersects with any of these (except itself). If it does, then we create a white square, named intersectionSquare, and set its frame to be equal to the intersection frame.
And to save up your memory usage, you can delete the square from the scene and set intersectionSquare to nil if there is no intersection at all.
Final result:
The final result would look like this:
That's just a rapid draft that I made to show you on you could approach the problem, and obviously there are many things that you could add or improve (apply this to a situation where you have not only 2 but many squares on screen, or create a kind of magnetism effect for when your user release his finger from the screen, etc) but I hope at least it will put you on the right track for your project :)
I am new to Swift and SpriteKit and am learning to understand the control in the game "Fish & Trip". The sprite node is always at the center of the view and it will rotate according to moving your touch, no matter where you touch and move (hold) it will rotate correspondingly.
The difficulty here is that it is different from the Pan Gesture and simple touch location as I noted in the picture 1 and 2.
For the 1st pic, the touch location is processed by atan2f and then sent to SKAction.rotate and it is done, I can make this working.
For the 2nd pic, I can get this by setup a UIPanGestureRecognizer and it works, but you can only rotate the node when you move your finger around the initial point (touchesBegan).
My question is for the 3rd pic, which is the same as the Fish & Trip game, you can touch anywhere on the screen and then move (hold) to anywhere and the node still rotate as you move, you don't have to move your finger around the initial point to let the node rotate and the rotation is smooth and accurate.
My code is as follow, it doesn't work very well and it is with some jittering, my question is how can I implement this in a better way? and How can I make the rotation smooth?
Is there a way to filter the previousLocation in the touchesMoved function? I always encountered jittering when I use this property, I think it reports too fast. I haven't had any issue when I used UIPanGestureRecoginzer and it is very smooth, so I guess I must did something wrong with the previousLocation.
func mtoRad(x: CGFloat, y: CGFloat) -> CGFloat {
let Radian3 = atan2f(Float(y), Float(x))
return CGFloat(Radian3)
}
func moveplayer(radian: CGFloat){
let rotateaction = SKAction.rotate(toAngle: radian, duration: 0.1, shortestUnitArc: true)
thePlayer.run(rotateaction)
}
var touchpoint = CGPoint.zero
var R2 : CGFloat? = 0.0
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches{
let previousPointOfTouch = t.previousLocation(in: self)
touchpoint = t.location(in: self)
if touchpoint.x != previousPointOfTouch.x && touchpoint.y != previousPointOfTouch.y {
let delta_y = touchpoint.y - previousPointOfTouch.y
let delta_x = touchpoint.x - previousPointOfTouch.x
let R1 = mtoRad(x: delta_x, y: delta_y)
if R2! != R1 {
moveplayer(radiant: R1)
}
R2 = R1
}
}
}
This is not an answer (yet - hoping to post one/edit this into one later), but you can make your code a bit more 'Swifty' by changing the definition for movePlayer() from:
func moveplayer(radian: CGFloat)
to
rotatePlayerTo(angle targetAngle: CGFloat) {
let rotateaction = SKAction.rotate(toAngle: targetAngle, duration: 0.1, shortestUnitArc: true)
thePlayer.run(rotateaction)
}
then, to call it, instead of:
moveplayer(radiant: R1)
use
rotatePlayerTo(angle: R1)
which is more readable as it describes what you are doing better.
Also, your rotation to the new angle is constant at 0.1s - so if the player has to rotate further, it will rotate faster. it would be better to keep the rotational speed constant (in terms of radians per second). we can do this as follows:
Add the following property:
let playerRotationSpeed = CGFloat((2 *Double.pi) / 2.0) //Radian per second; 2 second for full rotation
change your moveShip to:
func rotatePlayerTo(angle targetAngle: CGFloat) {
let angleToRotateBy = abs(targetAngle - thePlayer.zRotation)
let rotationTime = TimeInterval(angleToRotateBy / shipRotationSpeed)
let rotateAction = SKAction.rotate(toAngle: targetAngle, duration: rotationTime , shortestUnitArc: true)
thePlayer.run(rotateAction)
}
this may help smooth the rotation too.
In my game, there's a class for a "wall" that's moving to the left. I want to change the speed of it based on count i that I added to touchesBegan method:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent {
count++
}
func startMoving() {
let moveLeft = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: 1 )
let move = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: 0.5)
if(count <= 10)
{
runAction(SKAction.repeatActionForever(moveLeft))
}
else
{
runAction(SKAction.repeatActionForever(move))
}
}
but it's not working. Can you help?
As I said there are a lot of changes which have to be done:
First let's change MLWall class and add a duration property which will be used in startMoving method:
var duration:NSTimeInterval = 0.5
Then still inside MLWall class change the init method:
init(duration: NSTimeInterval) {
self.duration = duration
//...
}
And change startMoving method to use this passed parameter:
func startMoving() {
let moveLeft = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: self.duration)
runAction(SKAction.repeatActionForever(moveLeft))
}
Those are changes inside Wall class. Now let's make some changes in WallGenerator class:
First WallGenerator class should be aware of how fast walls should go. So we are adding property to store that info:
var currentDuration: NSTimeInterval = 1 // I named it duration, because SKAction takes duration as a parameter, but this actually affects on speed of a wall.
After that the first method which has to be changed is startGeneratingWallsEvery(second:) into startGeneratingWallsEvery(second: duration:
//duration parameter added
func startGeneratingWallsEvery(seconds: NSTimeInterval, duration : NSTimeInterval) {
self.currentDuration = duration
generationTimer = NSTimer.scheduledTimerWithTimeInterval(seconds, target: self, selector: "generateWall", userInfo: nil, repeats: true)
}
Here, we are making a WallGenerator aware of desired wall's speed.
And the next method which has to be changed in order to use that speed is:
//duration parameter added
func generateWall() {
//...
//Duration parameter added
let wall = MLWall(duration: self.currentDuration)
//...
}
And there is a GameScene left. There, I've added a tapCounter property:
let debugLabel = SKLabelNode(fontNamed: "Arial") //I've also added a debug label to track taps count visually
var tapCounter = 0
Here is how you can initialize label if you want to see number of tap counts:
//Setup debug label
debugLabel.text = "Tap counter : \(tapCounter)"
debugLabel.position = CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMaxY(frame)-50.0)
debugLabel.fontColor = SKColor.purpleColor()
self.addChild(debugLabel)
First I've changed the start method:
func start() {
//...
// adding duration parameter in method call
wallGenerator.startGeneratingWallsEvery(1,duration: 1)
}
The important part is : wallGenerator.startGeneratingWallsEvery(1,duration: 1) which says start generating walls every second with one second duration(which affects on node's speed).
Next, I've modified touchesBegan of the scene into this:
if isGameOver {
restart()
} else if !isStarted {
start()
} else {
tapCounter++
debugLabel.text = "Tap counter : \(tapCounter)"
if(tapCounter > 10){
wallGenerator.stopGenerating()
wallGenerator.startGeneratingWallsEvery(0.5, duration:0.5)
}
hero.flip()
}
Then, changed restart() method in order to restart the counter when game ends:
func restart() {
tapCounter = 0
//...
}
And that's pretty much it. I guess I haven't forgot something, but at my side it works as it should. Also, note that using NSTimer like from this GitHub project is not what you want in SpriteKit. That is because NSTimer don't respect scene's , view's or node's paused state. That means it will continue with spawning walls even if you think that game is paused. SKAction would be a preferred replacement for this situation.
Hope this helps, and if you have further questions, feel free to ask, but I guess that you can understand what's happening from the code above. Basically what is done is that WallGenerator has become aware of how fast their wall nodes should go, and Wall node has become aware of how fast it should go...
EDIT:
There is another way of changing walls speed by running an moving action with key. Then, at the moment of spawning, based on tapCounter value, you can access an moving action by the key, and directly change actions's speed property...This is probably a shorter way, but still requires some changes (passing a duration parameter inside Wall class and implementing tapCounter inside scene).
Try doing it like so :
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent {
count++
startMoving()
}
func startMoving() {
removeAllActions()
let moveLeft = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: count <= 10 ? 1.0 : 0.5)
runAction(SKAction.repeatActionForever(moveLeft))
}
As of I read comments below your question, you made a really unclear question, but nevertheless you have idealogical problems in your code.
Your touchesBegan method is implemented in the wall if I understood everything right, so it has no effect on newly generated walls. You have to move that logic to the scene and then spawn new walls with speed as a parameter, or at least make count a class var, so every wall can access that, but you still has to handle your touches in the scene, because now touch is handled when user taps directly in the wall.
I'm experimenting round with #nickfalk's answer on How to rotate sprite in sprite kit with swift on how to rotate a sprite in sprite kit.
How would I adjust this to gradually increase rotation speed up to a maximum, then when the screen is clicked, it gradually slows down and goes in the reverse direction for x amount of time?
Thanks!
Toby.
Ok, the following (slightly messy proof of concept) spins a sprite at a constant speed. Upon tap+hold it gradually slows the rotation to a halt. Ending the touch immediately returns the rotation to full speed.
I've set up a scene with the following properties: var sprite : SKSpriteNode? and var shouldDecelerate = false:
The sprite is set up with the preferred details and have a repeactActionForever-action running a 360 degrees rotation. From here its fairly straightforward:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
shouldDecelerate = true
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
shouldDecelerate = false
sprite?.speed = 1
sprite!.runAction(SKAction.speedTo(sprite!.speed, duration: 1/60))
}
override func update(currentTime: CFTimeInterval) {
if let sprite = sprite {
if sprite.speed > 0 && shouldDecelerate {
let newSpeed = max(sprite.speed - 0.1, 0) // we don't want a negative speed as it will reverse the rotation
sprite.runAction(SKAction.speedTo(newSpeed, duration: 1/60))
}
}
}
If you want a gradual increase in speed you basically just need an if with opposite logic of the one I've included in update() above, oh and you should also remove the sprite?.speed = 1 line in touchesEnded().
If you need to have other move-actions where the speed is not effected by the rotation-speed I suggest you hook the sprite up to an SKNode and let this handle the other actions.