Can MotionDetection in Swift perform 2 different actions? - swift

I am detecting when a user shakes his phone. Is it possible to perform 2 independent actions? I mean first do this1, then do this2.
override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
// first this needs to be done
let scene = makeScene()
animationView.frame.size = scene.size
animationView.presentScene(scene)
// then this needs to be done
let scene2 = makeScene2()
animationView.frame.size = scene2.size
animationView.presentScene(scene2)
}
}
When I do that only the second one is executed

Instead of dealing with hardcoded delays, try wrapping your animation in a CATransaction and run your 2nd step in its completion handler:
override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
CATransaction.begin()
let scene = makeScene()
animationView.frame.size = scene.size
animationView.presentScene(scene)
CATransaction.setCompletionBlock {
let scene2 = self.makeScene2()
self.animationView.frame.size = scene2.size
self.animationView.presentScene(scene2)
}
CATransaction.commit()
}
}

Solved the problem by adding a delay between the actions.
override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
let scene = makeScene()
animationView.frame.size = scene.size
animationView.presentScene(scene)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4), execute: {
let scene2 = self.makeScene2()
self.animationView.frame.size = scene2.size
self.animationView.presentScene(scene2)
})
}
}
}

Related

How can I access the indices of a hit test?

I am trying to add a different object to a scene with a hit test, and I am currently only able to add one object. I want to access the second, third and fourth indices in my hit test results in order to to add these objects with a touch.
I have tried accessing through .indices but it is not compatible with UITouch.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let touchLocation = touch.location(in: sceneView)
let results = sceneView.hitTest(touchLocation, types: .featurePoint)
if let hitResult = results.first {
let cubeScene = SCNScene(named: "art.scnassets/cube.scn")!
if let cubeNode = cubeScene.rootNode.childNode(withName: "cube", recursively: true) {
sceneView.scene.rootNode.addChildNode(cubeNode)
}
}
}
}
How should I go about this? I have thought about creating one SCNScene, pass it as an array, and then would load all the models from the scene.
As Samual suggested... example
var hitTestOptions = [SCNHitTestOption: Any]()
#objc func handleTap(recognizer: UITapGestureRecognizer)
{
let location: CGPoint = recognizer.location(in: gameScene)
let hitResults: [SCNHitTestResult] = gameScene.hitTest(location, options: hitTestOptions)
for vHit in hitResults
{
if(vHit.node.name?.prefix(5) == "Panel")
{
gameControl.selectPanel(vPanel: vHit.node.name!)
return
}
}
}

Button is still working even when removeFromParent() in Swift SpriteKit is called

In the code below if I press the removeButton the button would disappear but it's still working. Maybe I should make the if statement false but I don't know how.
class GameScene : SKScene {
var button : SKSpriteNode!
var removeButton : SKSpriteNode!
var image : SKSpriteNode!
override func didMove(to view: SKView) {
createButton()
createRemoveButton()
}
func createButton() {
button = SKSpriteNode(imageNamed: "button")
button.position = CGPoint(x: -300,y: 0)
self.addChild(button)
}
func createRemoveButton() {
removeButton = SKSpriteNode(imageNamed: "removeButton")
removeButton.position = CGPoint(x: 300,y: 0)
self.addChild(removeButton)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self)
if button.contains(touchLocation) {
image = SKSpriteNode(imageNamed: "image")
image.position = CGPoint(x: 0,y: 300)
self.addChild(image)
}
if removeButton.contains(touchLocation) {
button.removeFromParent()
}
}
override func update(_ currentTime: TimeInterval) {
}
}
You need to understand ARC (Automatic Reference Counting) and what it takes to retain an instance.
In your case you are retaining button, which means it does not delete from memory.
Now when you remove the button from the parent, the button's frame is still exactly the same, the only thing different is button.parent is now nil
When you call button.contains(touchLocation), this is going to pass because you are not concerned with whether or not button is on the scene, all you are doing is checking if the touchLocation is within the frame of the button.
The quickest fix is to check the parent:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self)
if button.parent == self && button.contains(touchLocation) {
image = SKSpriteNode(imageNamed: "image")
image.position = CGPoint(x: 0,y: 300)
self.addChild(image)
}
if removeButton.contains(touchLocation) {
button.removeFromParent()
}
}
In reality though, you need to learn how to better manage your resources. I would recommend trying to find tutorials about how ARC works.
It is normal because you dont use the good button. You use button instead of removeButton.
You should do:
if removeButton.contains(touchLocation) {
removeButton.removeFromParent()
}
Now it should work correctly

UITapGesture and TouchesBegan are producing different locations in view

I'm using SKTileMaps and a CameraNode as well as GameplayKit and SpriteKit
The code I want to work is incorrectly finding a location in the "view"
func handleTapFrom(_ sender: UITapGestureRecognizer)
{
let location = sender.location(in: self.view)
if (map.contains(location))
{
let tRow = map.tileRowIndex(fromPosition: location)
let tColumn = map.tileColumnIndex(fromPosition: location)
let movePosition = map.centerOfTile(atColumn: tColumn, row: tRow)
let moveAction = SKAction.move(to: movePosition, duration:1.0)
cam.run(moveAction)
}
}
and the code that is working correctly is finding locations in self (rather than self.view) and this is correct
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
let firstTouch = touches.first?.location(in: self)
if (map.contains(firstTouch!))
{
let tRow = map.tileRowIndex(fromPosition: firstTouch!)
let tColumn = map.tileColumnIndex(fromPosition: firstTouch!)
let movePosition = map.centerOfTile(atColumn: tColumn, row: tRow)
let moveAction = SKAction.move(to: movePosition, duration:1.0)
cam.run(moveAction)
}
}
The columns and rows are correct based on the location, the problem I am having is that the locations (xfloat, yfloat) are different between the two functions even when the touch is at the same location
These two bits of code are all in the class GameScene, but I cannot figure out why they have different locations? And how to fix the tap gesture so that the locations it finds are the same as those found in touchesBegan.
Sorry if this is confusing or unintelligible. I welcome an opportunity to clarify.
In your handleTapFrom function the location should come from convertPoint.
func handleTapFrom(_ sender: UITapGestureRecognizer) {
if sender.state != .ended {
return
}
let location = sender.location(in: sender.view!)
let targetLocation = self.convertPoint(fromView: location)
if map.contains(targetLocation) {
// Your code here
}
}

SpriteKit: How to identify the previous scene the user is coming from?

I have a GameOver scene with a button to a Settings scene. (Code shown below) I want to add a button on the MainMenu scene to do the same, send the user to the Settings scene but I want just one “Back” button on the Settings scene. I'm assuming this can be accomplished with an IF statement. If the user comes from A scene move to B else if the user comes from X scene move to Y scene.
How can I tell in SpriteKit which scene is the user coming from?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches {
let pointOfTouch = touch.location(in: self)
let nodeUserTapped = atPoint(pointOfTouch)
if nodeUserTapped.name == "BackButton" {
let sceneToMoveTo = SKScene(fileNamed: "GameOverScene")
let gameTransition = SKTransition.fade(withDuration: 0.5)
self.view!.presentScene(sceneToMoveTo!, transition: gameTransition)
}
}
}
In this case you can adopt various solutions to achieve what you wish.
I want to show you just 2 example, a rapid and simple way and a more custom way.
1: - You can use the userData instance property of your next scene to store the current scene, something like:
sceneToMoveTo.userData = NSMutableDictionary()
sceneToMoveTo.userData?.setObject("MenuScene", forKey: "previousScene" as NSCopying)
and when you are in the next scene you can ask to this NSMutableDictionary who is the previous scene as:
guard let previousValue = self.userData?.value(forKey: "previousScene") else { return }
switch previousValue as! String {
case "MenuScene":
//do whatever you want if the previous scene was MenuScene
default:
break
}
2: - You can build an enumeration that contain a list of the scenes where you want to move.
After that, each scene contain a simple var to store the previous scene: it's default is .None but you should fill it when you call the new scene. When you arrive to the SettingsScene and press your back button you can ask to this previousScene var who is the previous scene and decide, through a switch statement, where to go.
// GAMEOVER SCENE
import SpriteKit
enum Scenes : Int {
case MainMenu = 0
case Settings = 1
case GameOver = 2
case None = 3
}
class GameOver: SKScene {
var previousScene:Scenes = .None
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches {
let pointOfTouch = touch.location(in: self)
let nodeUserTapped = atPoint(pointOfTouch)
if nodeUserTapped.name == "settingsBtn" {
if let sceneToMoveTo = SKScene(fileNamed: "SettingsScene") {
let gameTransition = SKTransition.fade(withDuration: 0.5)
(sceneToMoveTo as! SettingsScene).previousScene = .GameOver
self.view?.presentScene(sceneToMoveTo, transition: gameTransition)
}
}
}
}
}
// SETTINGS SCENE
import SpriteKit
class SettingsScene: SKScene {
var previousScene:Scenes = .None
override func didMove(to view: SKView) {
print("Current scene loaded: \(type(of:self))")
print("Previous scene was: \(previousScene)")
let label = SKLabelNode.init(text: "BackButton")
addChild(label)
label.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
label.name = "backBtn"
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches {
let pointOfTouch = touch.location(in: self)
let nodeUserTapped = atPoint(pointOfTouch)
if nodeUserTapped.name == "backBtn" {
switch previousScene {
case .MainMenu: // go back to menu
if let sceneToMoveTo = SKScene(fileNamed: "MenuScene") {
let gameTransition = SKTransition.fade(withDuration: 0.5)
(sceneToMoveTo as! MenuScene).previousScene = .Settings
self.view?.presentScene(sceneToMoveTo, transition: gameTransition)
}
break
case .GameOver: // re-launch a new game
print("The game was ended")
if let sceneToMoveTo = SKScene(fileNamed: "GameScene") {
let gameTransition = SKTransition.fade(withDuration: 0.5)
(sceneToMoveTo as! GameScene).previousScene = .Settings
self.view?.presentScene(sceneToMoveTo, transition: gameTransition)
}
default:
break
}
}
}
}
}
//MENU SCENE
import SpriteKit
class MenuScene: SKScene {
var previousScene:Scenes = .None
override func didMove(to view: SKView) {
print("Current scene loaded: \(type(of:self))")
print("Previous scene was: \(previousScene)")
}
}
userData is your friend here, it is a dictionary that can be found on any type of SKNode. All you need to do is store the current scene you are in into the userData, and make the transition. This will keep your current scene in memory. Since your current scene is in memory and is not a part of the new scenes node tree, no update method will be called on it. Essentially you have a frozen scene state. Then when you are ready to return to the scene, just pull the instance out of the user data, and present it. Viola, you have transitioned back to your scene the moment that you have left it previously.
Basically, if you want the scene to resume where it left off, do the following:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches {
let pointOfTouch = touch.location(in: self)
let nodeUserTapped = atPoint(pointOfTouch)
if nodeUserTapped.name == "SettingButton" {
let sceneToMoveTo = SKScene(fileNamed: "SettingScene")
sceneToMoveTo.userData = ["previousScene":self.scene]
let gameTransition = SKTransition.fade(withDuration: 0.5)
self.view!.presentScene(sceneToMoveTo!, transition: gameTransition)
}
}
}
Then, at any point you want to return to the previous scene, just add the following.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches {
let pointOfTouch = touch.location(in: self)
let nodeUserTapped = atPoint(pointOfTouch)
if nodeUserTapped.name == "BackButton" {
if let userData = self.userData, let previousScene = userData["previousScene"] as? SKScene
{
let gameTransition = SKTransition.fade(withDuration: 0.5)
self.view!.presentScene(previousScene, transition: gameTransition)
}
}
}
}

How would I remove this function when the game is over?

I have this function and I want to remove it once the game is over. I tried using removeActionWithKey but that doesn't seem to work. Is there another way I could remove it once the game is over?
func doAction() {
let generateCircles = SKAction.sequence([
SKAction.runBlock(self.circleRandom),
SKAction.waitForDuration(1.5)])
let endlessAction = SKAction.repeatActionForever(generateCircles)
runAction(endlessAction)
}
if firstBody.categoryBitMask == HeroCategory && secondBody.categoryBitMask == RedCategory {
//Tried to remove the func here but that doesn't work.
removeActionForKey("stop")
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
touchingScreen = false
if let touch = touches.first {
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if node.name == "start" {
startGame.removeFromParent()
//Calling the doAction func here once start button is pressed.
let action = SKAction.runBlock(doAction)
runAction(action, withKey: "stop")
}
}
}
replace runAction(endlessAction) with runAction(endlessAction, withKey: "endlessAction1")
then call
removeActionForKey("stop")
removeActionForKey("endlessAction1")
where you want it to stop.