Why SKNode isn't reacting to touch in SpriteKit game? - xcode

In order to make a pause in a SpriteKit game I made a pause button. It worker like a charm when I wanted to make an another scene for pause and respond to touch was transitioning to a scene. My function then looked like this (P.S. pause = pause button) But there were other problems with it and I decided to make pause within my main scene: playscene
So I added a pauseBackground and resumeButton that is attached to pause background and hided it (declared resumeButton.hidden = true)
Now my function looks like this:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
if self.onGround {
self.speedY = -20.0
self.onGround = false
}
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if self.nodeAtPoint(location) == self.pause {
self.scene?.view?.paused = true
self.pauseBackground.hidden = false
self.resumeButton.hidden = false
if self.nodeAtPoint(location) == self.resumeButton {
self.pauseBackground.hidden = true
self.resumeButton.hidden = true
self.scene?.view?.paused = false
}
}
}
}
It's interesting because it worked with scene transitions, but now Xcode can't detect or ignore my tap in pause button location. I don't really know why. Could you please help me?
Update: I tried switch statement:
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
switch self.nodeAtPoint(location) {
case self.pause:
self.scene?.view?.paused = true
self.pauseBackground.hidden = false
self.resumeButton.hidden = false
case self.resumeButton:
self.pauseBackground.hidden = true
self.resumeButton.hidden = true
self.scene?.view?.paused = false
default:
println()
}
}
Sadly, it's not working either! I really have no idea why it's not detecting a touch in that location.

Give a name to your pause button. Control if your pause button is touched in touchesBegan by printing the name of the touched node.
Control the zPosition of the touched node and the pause button. Set the zPosition of the pause button = 1000 and try touching it.
EDIT:
Let's say, the node node1 is the parent node of the pauseButton.
node1.zPosition = 100.0f;
pauseButton.zPosition = 200.0f;
the continueButton is above the scene
continueButton.zPosition = 250.0f;
That means, that the zPosition of the pauseButton is 200 above the parent node node1. The zPosition of the continueButton (250) is less than the zPosition of the pauseButton above the scene, although the zPosition seems to be less (200). That's because the zPosition of the pauseButton is 300 above the scene. The zPosition of the parent node is very important to control the zPositions of its children and the other nodes of the scene.
zPosition of a node is always relative to its parent node. Have your both buttons (pause and resume button) the same parent node? The best way to control the touched node, is to give each node a name and a zPosition, and to print this (name and zPosition with NSLog() / println()) in the touchesBegan. With this you know exactly which node is touched and you know its zPosition. And so you can correct the zPosition of each node, so that it works as you need.

Related

Updating rotary knob in real time with SwiftUI

I'm currently trying to convert a slider in to a rotary knob and having a tough time of it all. The knob works in design but i'm struggling to set the correct value within the knob and as a result change the value within the app in real time.
I'm using AVAudio to set up an engine for people to record with that has effects like Reverb and Delay.
The reverb value is set as followed within the Audio Class:
#Published var reverbValue: Float = 0.0
and later on referenced in a function to change it's value
func changeReverbValue() {
setReverb.wetDryMix = reverbValue
}
When I use a regular slider as follows the change works:
Slider(value: $recordingsettings.reverbValue, in: Float(0.0)...recordingsettings.reverbMaxValue, onEditingChanged: { _ in
self.recordingsettings.changeReverbValue()
}).accentColor(Color.white)
As mentioned the knob works fine in its design:
ZStack {
Knobs(color: .orange)
.rotationEffect(
.degrees(max(0, initialCircleState()))
)
.gesture(DragGesture(minimumDistance: 0)
.onEnded({ _ in
startDragValue = -1.0
})
.onChanged { dragValue in
let touchDifferential = touchDifference(dragValue)
setInitialDragVal()
let computedTouch = computeTouch(touchDifferential)
print(computedTouch)
baseValue = getBaseVal(computedTouch)
let normalizeVal = baseValue / touchAmt
value = Float(normalizeVal * rngOffset(range: bounds) + bounds.lowerBound)
print("vaule is: \(value)")
}
)
GrayCircle(bounds: bounds)
OrangeCircle(baseValue: $value, bounds: bounds)
}
.rotationEffect(bounds.lowerBound < 0 ? .degrees(90) : .degrees(107))
I've had some success connecting the knob to the reverb value to the point where the slider also moves when the rotary knob does, however the changeReverbValue function doesn't work.
The success comes from setting the value within the knob view as follows:
#Binding var value: AUValue
And then referencing the knob on the same struct of the main view as the slider:
Knob(value: $recordingsettings.reverbValue, bounds: 0...CGFloat(recordingsettings.reverbMaxValue))
.onTapGesture {
self.recordingsettings.changeReverbValue()
}
The on tap gesture was a way in which I thought it might call the change reverb value function when the knob was turned but to no avail.
The binding value passed in the knob also has other challenges. For some reason when I playback audio without headphones and then turn the knob the audio starts to stutter. This doesn't happen with headphones and I find that pretty weird.
Anyone know how I could reference the reverb value within the rotary knob and have the changeReverbValue function called at the same time?
I just want to replace the slider with something that looks better. Otherwise i'm going to have to leave this for a bit and just implement the sliders instead throughout the app.
If I don't set the value of the knob as #binding in the rotary knob view the track doesn't stutter on playback but then I don't know if it's possible to change the reverb value without a #binding var.
I struggled to parse a precise singular problem statement from the narrative, so this is perhaps just an off-base commentary and not a solution. I walked away thinking your problem is: a custom UI component is "jumpy/stuttery" during interaction and produces similarly punctate effects on app state.
If that's fair, I worked around the same issue in my first SwiftUI app. The cause could be two things:
Not using the right async queue by accident.
Forcing an #State or #Published property to update for all global state changes. That means you are pushing stale state from earlier back into an interaction, possibly with a circular feedback loop.
The solution is pretty simple. Request and consume model updates with both a value and a source tag. Throttle and filter out the self-tag to keep local state responsive to only one just-in-time data stream.
I used that pattern in that first app (free, Inclusivity for Mac) to coordinate an HSV color wheel and color channel custom slider components. The wheel, sliders, and other interactions feed/read a shared Combine pipeline (CurrentValueSubject<SourcedColorVector,Never>.erasedToAny()).
Some sample gestures, which simply punt the gating work to a view model:
The HSVWheel drag-around or click gesture
private func touchUpInWheel() -> ExclusiveGesture<_ChangedGesture<DragGesture>, _EndedGesture<DragGesture>> {
ExclusiveGesture(
DragGesture(minimumDistance: 10, coordinateSpace: .named(wheel))
.onChanged { change in
let adjusted = CGPoint(x: change.translation.width - targetDiameter + change.startLocation.x / 2,
y: change.translation.height - targetDiameter + change.startLocation.y / 2)
vm.setHueSat(drag: adjusted)
},
DragGesture(minimumDistance: 0, coordinateSpace: .named(wheel))
.onEnded { end in
let click = CGPoint(x: end.location.x - vm.radius,
y: end.location.y - vm.radius)
vm.setHueSat(click: click)
}
)
}
A typical slider gesture (this is the vertical value slider)
private func tapAndDrag() -> _EndedGesture<_ChangedGesture<DragGesture>> {
DragGesture(minimumDistance: 0,
coordinateSpace: .named(valuePickerSpace))
.onChanged { value in
let location = value.location.y - .valueSliderGestureOffset
vm.setValueKnobLocation(raw: location)
}
.onEnded { end in
let location = end.location.y - .valueSliderGestureOffset
vm.setValueKnobLocation(raw: location)
}
}

How do I skip to a position in the animation with an animationplayer in Godot?

I'm making a game in godot and have to store the animation position because the animation gets suspended by another one. I want to continue the animation where it was before and that's why I have to store the animation position and then set it to the stored value.
I've tried setting it (didn't work) and in the documentation and other places in the internet I haven't found anything helpful.
This is the script on it:
extends KinematicBody2D
onready var animation_player = $AnimationPlayer
var hurt_anim_playing: bool = false
var hurt_anim_progress: float = 0
func _ready():
animation_player.play("idle")
pass
func _physics_process(delta):
# for each touching heart, get hurt
for body in hitbox.get_overlapping_bodies():
if body.has_method("heart"):
G.health -= 1
hurt_anim_playing = true
hurt_anim_progress = animation_player.current_animation_position
animation_player.play("hurt")
update_sprite()
body.queue_free()
func die():
dieLayer.visible = true
get_tree().paused = true
func update_sprite():
sprite.frame = G.max_health - G.health
if G.health == 0:
die()
func _on_AnimationPlayer_animation_finished(anim_name):
if anim_name == "hurt":
hurt_anim_playing = false
animation_player.play("idle")
animation_player.current_animation_position = hurt_anim_progress
Actually I wanted to set the animation position and let the animation continue where it stopped, but instead I got an error
The problem is that current_animation_position has only a getter, not a setter:
https://docs.godotengine.org/en/3.1/classes/class_animationplayer.html#class-animationplayer-property-current-animation-position
To set the animation to a specific point, you can use advance(float delta), if you want to handle everything between start and the resuming point or seek(float seconds, bool update=false), if you just want to jump to the new position. You may test out, if you need to set update to true. The documentation says "If update is true, the animation updates too, otherwise it updates at process time."
Your code would simply look like this:
func _physics_process(delta):
# for each touching heart, get hurt
for body in hitbox.get_overlapping_bodies():
if body.has_method("heart"):
G.health -= 1
hurt_anim_playing = true
hurt_anim_progress = animation_player.current_animation_position
animation_player.play("hurt")
update_sprite()
body.queue_free()
func _on_AnimationPlayer_animation_finished(anim_name):
if anim_name == "hurt":
hurt_anim_playing = false
animation_player.play("idle")
animation_player.seek(hurt_anim_progress) #maybe (hurt_anim_progress, true)

Where to put grand central dispatch code?

I have set up some code that looks like this
let queue = DispatchQueue(label: "queue.1", qos: .utility, attributes: .concurrent)
queue.async {
AppUtility().run(X: self.BitCount)
}
Here is the function that is called:
public func run(X: UILabel) {
var placeHolder = 0
while placeHolder == 0{
if globalValues.bitsPerSecond != 0 {
globalValues.Bits = globalValues.Bits + 1
//globalValues.bitsPerSecond
print("Ran")
}
UserDefaults.standard.set(globalValues.Bits, forKey: "bits")
X.text = "\(globalValues.Bits)"
sleep(1)
}
}
This code will update a label everysecond through a counter while the user is looking at the screen. From what I have read I believe my code to be correct however I have no Idea where to put it so it can always run in the background. Any help?
I have no Idea where to put it so it can always run in the background
In iOS, code doesn't run in the background. When your app is backgrounded, it is suspended.
If the idea is that the label should have the right value when the app comes back into the foreground, as if the counting had gone on while in the background, then just use the activation event as a signal to calculate how long we were backgrounded and adjust the label value accordingly.

SceneKit - Stop continuously looping Collada animation

I am using SceneKit to load a Collada (.dae) file.
Whatever I have tried, the animation repeats in a continuous loop whereas I only want it play once and then stop.
Here's the loading code where sceneView is my SCNView:
//Load the scene and play
let scene = SCNScene(named: "Scenes.scnassets/test4.dae")
sceneView.scene = scene
sceneView.autoenablesDefaultLighting = true
sceneView.allowsCameraControl = true
The scene loads correctly and the animation plays on loading as required, but in a continuous loop.
I have tried removing all animations as follows:
sceneView.scene?.rootNode.removeAllAnimations()
but this seems to have no effect.
I have also tried retrieving all animations and setting the repeatCount = 1 or even to 0
let url = NSBundle.mainBundle().URLForResource("Scenes.scnassets/test4", withExtension: "dae")
let sceneSource = SCNSceneSource(URL:url!, options: nil)
let animationIndentifiers = sceneSource?.identifiersOfEntriesWithClass(CAAnimation)
if let ids = animationIndentifiers
{
for id in ids {
let animation: CAAnimation = sceneSource!.entryWithIdentifier(id as! String, withClass: CAAnimation.self)! as! CAAnimation
animation.repeatCount = 1
}
}
I have also tried this another way:
let keys = sceneView.scene!.rootNode.animationKeys() //get all the animation keys
if let theKeys = keys {
for key in theKeys {
let animation = sceneView.scene?.rootNode.animationForKey(key as! String)
animation?.repeatCount = 1
}
}
but again no luck.
I can find nothing in the Collada file itself that is obviously causing the animation to repeat. However, I notice that the .dae file icon on my Mac disk has a play button and clicking this also plays the animation within the icon on a continuous loop.
Update:
I now notice that in the code above I am setting the constant 'animation' attributes and this is not copied back to the actual scene nodes. Also, the only animation in the .dae file is in a child node. So here's my next attempt:
//Get the child nodes
let children = scene?.rootNode.childNodes
//Initialise a childNode counter
var childCount = 0
//Iterate through the child nodes
if let theChildren = children {
for child in theChildren {
let keys = child.animationKeys() //get all the animation keys
//Iterate through the animations
if let theKeys = keys {
for key in theKeys {
let animation = child.animationForKey(key as! String)
println("Initial Repeat count: \(animation.repeatCount)")
animation.repeatCount = 1
//Remove existing animation
scene?.rootNode.childNodes[childCount].removeAnimationForKey(key as! String)
//Add amended animation
scene?.rootNode.childNodes[childCount].addAnimation(animation, forKey: key as! String)
}
}
childCount++
}
}
There is actually only one animation attached to one child node. (I have also tried setting this using the the actual animation id string in place of 'key' above.)
The above shows Initial Repeat count: inf
and on checking afterwards it is indeed set to 1.
However, the animation still runs in an infinite loop :-(
Any help to resolve this would be much appreciated.
Further update
I have now created a new Collada file with simple animation using Maya and for some reason one of the trials attempted above actually works:
func sceneSetup() {
let scene = SCNScene(named: "Scenes.scnassets/test10.dae")
let children = scene?.rootNode.childNodes
var childCount = 0
if let theChildren = children {
for child in theChildren {
let keys = child.animationKeys() //get all the animation keys
if let theKeys = keys {
for key in theKeys {
let animation = child.animationForKey(key as! String)
animation.repeatCount = 1
scene?.rootNode.childNodes[childCount].removeAnimationForKey(key as! String)
scene?.rootNode.childNodes[childCount].addAnimation(animation, forKey: key as! String)
}
}
childCount++
}
}
sceneView.scene = scene
sceneView.autoenablesDefaultLighting = true
}
if anybody can explain that would be great!
Ah, but there's another problem!
Here's the start frame of the animation:
and here's the end frame where we want to end up:
But at the end of the animation the scene jumps back to the start view.
I have 'fixed' this by amending the animation so that frame 1 is copy of the final frame. This works and isn't noticeable but doesn't seem a very elegant solution.
For the animation jumping back to the first frame, the isAppliedOnCompletion value is what you are looking for.
animation.repeatCount = 1
animation.isAppliedOnCompletion = true
This will make sure that the animation pauses on the final frame.
I know this is an old question, but I came across this same problem and found a solution for the jumping back to the beginning issue. If you set the animation to not be removed on completion, the object should stay at the end location:
if let animation = child.animationForKey(child.animationKeys.first!) {
animation.repeatCount = 1
animation.removedOnCompletion = false
...
This works for me. It's based off your answer but will go through the node's entire tree and limit all of their animation counts to one.
Personally I found that if I missed any of the descendant nodes when manually traversing and setting the node animation counts to one, I would have a problem. This ensures that the node passed in itself, and all child nodes will only animate once, and then hold the model in place after finishing.
Use like this animateEntireNodeTreeOnce(mostRootNode: nodeYouImportedFromCollada).
func animateEntireNodeTreeOnce(mostRootNode node: SCNNode){
onlyAnimateThisNodeOnce(node)
for childNode in node.childNodes {
animateEntireNodeTreeOnce(mostRootNode: childNode)
}
}
func onlyAnimateThisNodeOnce(_ node: SCNNode) {
if node.animationKeys.count > 0 {
for key in node.animationKeys {
let animation = node.animation(forKey: key)!
animation.repeatCount = 1
animation.isRemovedOnCompletion = false
node.removeAllAnimations()
node.addAnimation(animation, forKey: key)
}
}
}
I Agree with #Phil Dudas and I like to add also this workaround to Prevent The Animation from Start By Default By Reducing Speed to Zero, When you set Speed to Zero it will remain at First state,
nodeAnimation.speed = 0
animation.repeatCount = 1
nodeAnimation.animation.isRemovedOnCompletion = false

UICollectionView effective drag and drop

I am currently trying to implement the UITableView reordering behavior using UICollectionView.
Let's call a UItableView TV and a UICollectionView CV (to clarify the following explanation)
I am basically trying to reproduce the drag&drop of the TV, but I am not using the edit mode, the cell is ready to be moved as soon as the long press gesture is triggered. It works prefectly, I am using the move method of the CV, everything is fine.
I update the contentOffset property of the CV to handle the scroll when the user is dragging a cell. When a user goes to a particular rect at the top and the bottom, I update the contentOffset and the CV scroll. The problem is when the user stop moving it's finger, the gesture doesn't send any update which makes the scroll stop and start again as soon as the user moves his finger.
This behavior is definitely not natural, I would prefer continu to scroll until the user release the CV as it is the case in the TV. The TV drag&drop experience is awesome and I really want to reproduce the same feeling. Does anyone know how they manage the scroll in TV during reordering ?
I tried using a timer to trigger a scroll action repeatedly as long as the gesture position is in the right spot, the scroll was awful and not very productive (very slow and jumpy).
I also tried using GCD to listen the gesture position in another thread but the result is even worst.
I ran out of idea about that, so if someone has the answer I would marry him!
Here is the implementation of the longPress method:
- (void)handleLongPress:(UILongPressGestureRecognizer *)sender
{
ReorganizableCVCLayout *layout = (ReorganizableCVCLayout *)self.collectionView.collectionViewLayout;
CGPoint gesturePosition = [sender locationInView:self.collectionView];
NSIndexPath *selectedIndexPath = [self.collectionView indexPathForItemAtPoint:gesturePosition];
if (sender.state == UIGestureRecognizerStateBegan)
{
layout.selectedItem = selectedIndexPath;
layout.gesturePoint = gesturePosition; // Setting gesturePoint invalidate layout
}
else if (sender.state == UIGestureRecognizerStateChanged)
{
layout.gesturePoint = gesturePosition; // Setting gesturePoint invalidate layout
[self swapCellAtPoint:gesturePosition];
[self manageScrollWithReferencePoint:gesturePosition];
}
else
{
[self.collectionView performBatchUpdates:^
{
layout.selectedItem = nil;
layout.gesturePoint = CGPointZero; // Setting gesturePoint invalidate layout
} completion:^(BOOL completion){[self.collectionView reloadData];}];
}
}
To make the CV scroll, I am using that method:
- (void)manageScrollWithReferencePoint:(CGPoint)gesturePoint
{
ReorganizableCVCLayout *layout = (ReorganizableCVCLayout *)self.collectionView.collectionViewLayout;
CGFloat topScrollLimit = self.collectionView.contentOffset.y+layout.itemSize.height/2+SCROLL_BORDER;
CGFloat bottomScrollLimit = self.collectionView.contentOffset.y+self.collectionView.frame.size.height-layout.itemSize.height/2-SCROLL_BORDER;
CGPoint contentOffset = self.collectionView.contentOffset;
if (gesturePoint.y < topScrollLimit && gesturePoint.y - layout.itemSize.height/2 - SCROLL_BORDER > 0)
contentOffset.y -= SCROLL_STEP;
else if (gesturePoint.y > bottomScrollLimit &&
gesturePoint.y + layout.itemSize.height/2 + SCROLL_BORDER < self.collectionView.contentSize.height)
contentOffset.y += SCROLL_STEP;
[self.collectionView setContentOffset:contentOffset];
}
This might help
https://github.com/lxcid/LXReorderableCollectionViewFlowLayout
This is extends the UICollectionView to allow each of the UICollectionViewCells to be rearranged manually by the user with a long touch (aka touch-and-hold). The user can drag the Cell to any other position in the collection and the other cells will reorder automatically. Thanks go to lxcid for this.
Here is an alternative:
The differences between DraggableCollectionView and LXReorderableCollectionViewFlowLayout are:
The data source is only changed once. This means that while the user is dragging an item the cells are re-positioned without modifying the data source.
It's written in such a way that makes it possible to use with custom layouts.
It uses a CADisplayLink for smooth scrolling and animation.
Animations are canceled less frequently while dragging. It feels more "natural".
The protocol extends UICollectionViewDataSource with methods similar to UITableViewDataSource.
It's a work in progress. Multiple sections are now supported.
To use it with a custom layout see DraggableCollectionViewFlowLayout. Most of the logic exists in LSCollectionViewLayoutHelper. There is also an example in CircleLayoutDemo showing how to make Apple's CircleLayout example from WWDC 2012 work.
As of iOS 9, UICollectionView now supports reordering.
For UICollectionViewControllers, just override collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath)
For UICollectionViews, you'll have to handle the gestures yourself in addition to implementing the UICollectionViewDataSource method above.
Here's the code from the source:
private var longPressGesture: UILongPressGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
longPressGesture = UILongPressGestureRecognizer(target: self, action: "handleLongGesture:")
self.collectionView.addGestureRecognizer(longPressGesture)
}
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case UIGestureRecognizerState.Changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case UIGestureRecognizerState.Ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
Sources:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UICollectionView_class/#//apple_ref/doc/uid/TP40012177-CH1-SW67
http://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering/
If you want to experiment rolling out your own, I just wrote a Swift based tutorial you can look. I tried to build the most basic of cases so as to be easier to follow this.
Here is another approach:
Key difference is that this solution does not require a "ghost" or "dummy" cell to provide the drag and drop functionality. It simply uses the cell itself. Animations are in line with UITableView. It works by adjusting the collection view layout's private datasource while moving around. Once you let go, it will tell your controller that you can commit the change to your own datasource.
I believe it's a bit simpler to work with for most use cases. Still a work in progress, but yet another way to accomplish this. Most should find this pretty easy to incorporate into their own custom UICollectionViewLayouts.

Resources