SpriteKit Controls / Gesture - xcode

Essentially I am trying to incorporate X2 gamescene buttons that do the following functions:
1) Tap to fly (this I have working)
2) Tap to shoot a projectile from Player position (I do not have working).
My problem is I currently have the fly func set when touched anywhere on the screen. I have tried the following :
This is in reference to my GameScene : I thought in order to split this out I would need a node on the screen to reference this function. This does not error in the console but does not appear in the GameScene.
// Button to trigger shooting :
let btnTest = SKSpriteNode(imageNamed: "Crater")
btnTest.setScale(0.2)
btnTest.name = "Button"
btnTest.zPosition = 10
btnTest.position = CGPoint(x: 100, y: 200)
self.addChild(btnTest)
Next in the Player class I have the following broken down:
var shooting = false
var shootAnimation = SKAction()
var noshootAnimation = SKAction()
Init func:
self.run(noshootAnimation, withKey: "noshootAnimation")
let projectile = SKSpriteNode(imageNamed: "Crater")
projectile.position = CGPoint (x: 100, y: 50)
projectile.zPosition = 20
projectile.name = "projectile"
// Assigning categories to Game objects:
self.physicsBody?.categoryBitMask =
PhysicsCategory.plane.rawValue
self.physicsBody?.contactTestBitMask =
PhysicsCategory.ground.rawValue
self.physicsBody?.collisionBitMask =
PhysicsCategory.ground.rawValue
self.physicsBody?.applyImpulse(CGVector(dx: 300, dy: 0))
self.addChild(projectile)
// Start the shoot animation, set shooting to true:
func startShooting() {
self.removeAction(forKey: "noshootAnimation")
self.shooting = true
}
// Stop the shoot animation, set shooting to false:
func stopShooting() {
self.removeAction(forKey: "shootAnimation")
self.shooting = false
}
The node appears in the GameScene which looks promising, finally I move to the last bit of code in the GameScene as follows:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) for touch in (touches) {
let location = touch.location(in: self)
let nodeTouched = atPoint(location)
if let gameSprite = nodeTouched as? GameSprite {
gameSprite.onTap()
}
// Check the HUD buttons which I have appearing when game is over…
if nodeTouched.name == "restartGame" {
// Transition to a new version of the GameScene
// To restart the Game
self.view?.presentScene(GameScene(size: self.size), transition: .crossFade(withDuration: 0.6))
}
else if nodeTouched.name == "returnToMenu"{
// Transition to the main menu scene
self.view?.presentScene(MenuScene(size: self.size), transition: . crossFade(withDuration: 0.6))
}
}
Player.startFly()
player.startShooting()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
Player.stopFly()
player.stopShooting()
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
Player.stopFly()
player.stopShooting()
}
override func update(_ currentTime: TimeInterval) {
player.update()
}
}
Unfortunately nothing happens in the GameScene and the node doesn’t fire when the screen is pressed, with the code above is there anyway I can amend this to allow for both ‘tap to fly’ + ‘tap to shoot’ functions. I still can’t figure out how to get the button I had declared early on in the GameScene to appear in which my gesture / touch position can be localised to this node on the screen as oppose to the whole screen in which I have currently..
I can say this sounded more simple in my head to begin with than actually coding together.

Firstly, the above code does not have any calls to run the animation with the key "shootAnimation" and is missing the call to re-run the not-shooting animation when stopShooting() and the call to re-run the shooting animation in startShooting(). The methods should include these as shown below
func startShooting() {
self.removeAction(forKey: "noshootAnimation")
// Add this call
self.run(shootAnimation)
self.shooting = true
}
func stopShooting() {
self.removeAction(forKey: "shootAnimation")
// Add this call
self.run(noshootAnimation)
self.shooting = true
}
Secondly, the noshootAnimation and shootAnimation animations are empty actions: they will not do anything if initialized as SKAction(). Depending on what you are trying to do, there are a number of class methods of SKAction that will create actions for you here.
Thirdly, any SKNode (recall that a scene is an SKNode subclass) will not receive touch calls if the node's isUserInteractionEnabled property is set to false (to which it is defaulted); instead, it will be treated as if its parent received the call. However, should a node's isUserInteractionEnabled be true, it will be the one that receives the UIResponder calls, not its parent node. Make sure that this property is set to true for the scene and for any nodes that need to receive touches (you can do this in didMove(to:) in the scene or elsewhere).
I will now propose an improvement. I frequently use buttons in Spritekit, but they are all subclasses of a custom button class (itself a subclass of SKSpriteNode) that uses protocol delegation to alert members of touch events. The scheme looks like this:
class Button: SKSpriteNode {
weak var responder: ButtonResponder?
// Custom code
// MARK: UIResponder
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
// Respond here
responder?.respond(to: self)
}
}
protocol ButtonResponder: AnyObject {
func respond(to button: Button)
}
class MyScene: SKScene, ButtonResponder {
func respond(to button: Button) {
// Do something on touch, but typically check the name
switch button.name {
case "myButton":
// Do something specific
default:
break
}
}
override func didMove(to view: SKView) {
// Set the scene as a responder
let buttonInScene = childNode(withName: "myButton") as! Button
buttonInScene.responder = self
}
}
For more on delegation, look here. Hopefully this helped a little bit.
EDIT: Convert touch location
You can convert the touch to the node on the screen by passing a reference to the location(in:) method of UITouch instead of the scene as you did in your code
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let yourNode = childNode(withName: "yourNode")!
for touch in touches {
let touchLocationToYourNode = touch.location(in: yourNode)
// Do something
}
}
Alternatively, use SKNode's convert(_:from:) and convert(_:to:) methods
let nodeA = SKNode()
let nodeB = SKNode()
nodeA.xScale = 1.5
nodeA.yScale = 1.2
nodeA.position = .init(x: 100, y: 100)
nodeA.zRotation = .pi
let pointInNodeACoordinateSystem = CGPoint(x: 100, y: 100)
let thatSamePointInNodeBCoordinateSystem = nodeB.convert(pointInNodeACoordinateSystem, from: nodeA)

Related

Reading right mouse clicks number of clicks

I have been experimenting with mouse clicks. I am ok with left mouse clicks getting raw values etc etc. I now want to add right mouse clicks. I have setup a basic example. What i would like to achieve if there is one mouse right click it performs one function, and if there is two mouse right clicks it performs a different function. The problem is if you do two mouse clicks it obviously cannot differentiate between the two and so fire the one mouse click function before performing the second mouse function.
I was thinking of maybe using a timer of some sort to record the number of click. But i end up going round in circles as i just seem to start the timers over and over. I'm hoping some one might help. thanks for reading here is the code.
Xcode 8 Swift 3 Mac OSX Sierra NOT IOS.. NOT IOS
import Cocoa
class MainWindowController: NSWindowController {
#IBOutlet weak var MyView: NSView!
override func windowDidLoad() {
super.windowDidLoad()
//Initialize mouse for Right Click numberOfClicksRequired = 1
let recogRightClick1 = NSClickGestureRecognizer(target: self, action: #selector(oneMouseClick))
recogRightClick1.buttonMask = 0x2
recogRightClick1.numberOfClicksRequired = 1
MyView.addGestureRecognizer(recogRightClick1)
//Initialize mouse for Right ClicknumberOfClicksRequired = 2
let recogRightClick2 = NSClickGestureRecognizer(target: self, action: #selector(twoMouseClick(myrec:myRightClick:)))
recogRightClick2.buttonMask = 0x2
recogRightClick2.numberOfClicksRequired = 2
MyView.addGestureRecognizer(recogRightClick2)
}//EO Overide
func oneMouseClick(myrec: NSPanGestureRecognizer,myRightClick:NSClickGestureRecognizer){
let rightClick = myRightClick.state.rawValue
print("oneMouseClick",rightClick)
}
func twoMouseClick(myrec: NSPanGestureRecognizer,myRightClick:NSClickGestureRecognizer){
let rightClick = myRightClick.state.rawValue
print("twoMouseClick",rightClick)
}
}//EnD oF thE wORld
UPDATE
I have re generated the code following the advice given. Now the code reflects more of what i wanted to do. My only problem is that I would like all the mouse operations to be triggered only inside 'myView' rather than within the main window. I thought it might have something to do with first responder but that doesn't seem to work. Again any thought would be appreciated. Please excuse any bad code i'm self taught.
Xcode 8 Swift 3 Mac OSX Sierra NOT IOS.. NOT IOS
import Cocoa
class MainWindowController: NSWindowController {
#IBOutlet weak var myView: NSView!
var mouseX:CGFloat = 0
var mouseY:CGFloat = 0
override func windowDidLoad() {
myView.window?.becomeFirstResponder()
myView.window?.acceptsMouseMovedEvents = true
super.windowDidLoad()
myView.wantsLayer = true
myView.layer?.backgroundColor = CGColor(red: 0.05, green: 0.57, blue: 0.80, alpha: 0.6)
NSEvent.addLocalMonitorForEvents(matching:.leftMouseDown){
self.mouseEventFunction(data: 1)
return $0
}
NSEvent.addLocalMonitorForEvents(matching:.leftMouseUp){
self.mouseEventFunction(data:2)
return $0
}
NSEvent.addLocalMonitorForEvents(matching:.rightMouseDown){
self.mouseEventFunction(data: 3)
return $0
}
NSEvent.addLocalMonitorForEvents(matching:.rightMouseUp){
self.mouseEventFunction(data: 4)
return $0
}
NSEvent.addLocalMonitorForEvents(matching:.mouseMoved) {
self.mouseX = NSEvent.mouseLocation().x
self.mouseY = NSEvent.mouseLocation().y
return $0 }
}//EO Overide
func mouseEventFunction (data: Int){
switch data{
case 1 :
print("LeftMouseDown")
case 2 :
print("LeftMouseUp")
case 3 :
print("RightMouseDown")
case 3 :
print("RightMouseUP")
default: break
}
if data == 1 {print("mouseX",mouseX,"mouseY",mouseY)}
}//eo mouseEvent
}//EnD oF thE wORld
UPDATE 2
I have now updated subClassing the view controller, so the mouse clicks are now only working in myView. I'm still having problems with 'func mouseDragged' What i need to achieve is the bottom left of my view is x = 0 and Y = 0. I had a try with converting but thats not working. hoping someone might guide me. thanks for reading here is the updated code.
Xcode 8 Swift 3 Mac OSX Sierra NOT IOS.. NOT IOS
import Cocoa
class MainWindowController: NSWindowController {
#IBOutlet weak var myView: NSView!
override func windowDidLoad() {
myView.window?.becomeFirstResponder()
myView.window?.acceptsMouseMovedEvents = true
window?.contentView?.addSubview(myView)
super.windowDidLoad()
}//EO Overide
}//EnD oF thE wORld
class testView: NSView {
override func draw(_ dirtyRect: NSRect) {
let backgroundColor = NSColor.lightGray
backgroundColor.set()
NSBezierPath.fill(bounds)
}
override func mouseDragged(with theEvent: NSEvent) {
let myLocationInWindow = theEvent.locationInWindow
let location = convert(myLocationInWindow, to: self)
Swift.print("myLocationInWindow",myLocationInWindow,"location",location)
}
override func mouseDown(with theEvent: NSEvent) {
Swift.print("mouseDown")
}
override func mouseUp(with theEvent: NSEvent) {
Swift.print("mouseUp clickCount: \(theEvent.clickCount)")
}
}//eo testView
To define mouse inside view you use
let myLocationInWindow = theEvent.locationInWindow
let location = convert(myLocationInWindow, from: nil)
where nil is the window
here is the final code
import Cocoa
class MainWindowController: NSWindowController {
#IBOutlet weak var myView: NSView!
override func windowDidLoad() {
super.windowDidLoad()
}//EO Overide
}//EnD oF thE wORld
class testView: NSView {
override func draw(_ dirtyRect: NSRect) {
let backgroundColor = NSColor.lightGray
backgroundColor.set()
NSBezierPath.fill(bounds)
}
override func mouseDragged(with theEvent: NSEvent) {
let myLocationInWindow = theEvent.locationInWindow
let location = convert(myLocationInWindow, from: nil)
Swift.print("location",location)
}
override func mouseDown(with theEvent: NSEvent) {
Swift.print("mouseDown")
}
override func mouseUp(with theEvent: NSEvent) {
Swift.print("mouseUp clickCount: \(theEvent.clickCount)")
}
}//eo testView

Changing default SKScene

I have a class called IntroLayer I'm trying to load as the inital scene when my game is launched. However after simply changing the GameScene to IntroScene as described in these simple steps, my IntroScene isn't being loaded. I set breakpoints on if let scene to see that it skips over it, and even set breakpoints in the actual IntroScene to verify didMoveToView is not being called. Any ideas?
I changed the let scene from GameScene to IntroScene in the GameViewController like so:
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let scene = IntroScene(fileNamed:"IntroScene") {
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
skView.presentScene(scene)
}
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .AllButUpsideDown
} else {
return .All
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
Then renamed GameScene.swift to IntroScene.swift and changed the class to this:
import SpriteKit
class IntroScene: SKScene {
override func didMoveToView(view: SKView) {
backgroundColor = UIColor.blackColor()
let fadeIn:SKAction = SKAction.fadeInWithDuration(1.0)
let fadeOut:SKAction = SKAction.fadeOutWithDuration(1.0)
let inspiredText = SKLabelNode(fontNamed: "Dead Kansas")
inspiredText.alpha = 0.0
inspiredText.text = "Inspired By"
inspiredText.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
addChild(inspiredText)
inspiredText.runAction(fadeIn)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
// let location = touch.locationInNode(self)
}
}
}
However when I launch the app, it doesn't appear that IntroScene is even called when I set breakpoints, and I'm getting a screen with a gray background.
You need to rename your GameScene.sks to IntroScene.sks
The sks file is an archived file for your scene data, if you open it up, you will notice you can change a lot of things about the scene, and even add nodes and sprites directly to it. When you call if let scene = IntroScene(fileNamed:"IntroScene") { you are unarchiving the sks file to be loaded as a scene, so anytime you want to create a new scene, remember that you will need this file.

NSClickGestureRecognizer not working on NSStatusItem

Trying to recognize a right click on a NSStatusItem I got a suggestion ( Thanks to Zoff Dino ) to use a NSClickGestureRecognizer for that. But for some bizarre reason it isn't working as it should be. I am able to recognize a left click (buttonMask = 0x1) but not a right-click (buttonMask = 0x2). This is how I would like it to work but it isn't:
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
if let button = statusItem.button {
// Add right click functionality
let gesture = NSClickGestureRecognizer()
gesture.buttonMask = 0x2 // right mouse
gesture.target = self
gesture.action = "rightClickAction:"
button.addGestureRecognizer(gesture)
}}
func rightClickAction(sender: NSGestureRecognizer) {
if let button = sender.view as? NSButton {
NSLog("rightClick")
}
}
UPDATE:
I still did not manage to gets to work. Somehow it doesn't react on a right click (but changing the code on a left click) does. I guess some really simple issues are occurring that seem to block it from working. Even stranger is the fact that gesture.buttonMask = 0x1 works on the left click.
An alternative solution rather than NSClickGestureRecognizer is to attach a custom view to the status bar and handle the event from there.
The small disadvantage is you have to take care of the drawing and menu delegate methods.
Here a simple example:
Create a file StatusItemView a subclass of NSView
import Cocoa
class StatusItemView: NSView, NSMenuDelegate {
//MARK: - Variables
weak var statusItem : NSStatusItem!
var menuVisible = false
var image : NSImage! {
didSet {
if image != nil {
statusItem.length = image.size.width
needsDisplay = true
}
}
}
//MARK: - Override functions
override func mouseDown(theEvent: NSEvent) {
if let hasMenu = menu {
hasMenu.delegate = self
statusItem.popUpStatusItemMenu(hasMenu)
needsDisplay = true
}
}
override func rightMouseDown(theEvent: NSEvent) {
Swift.print(theEvent)
}
//MARK: - NSMenuDelegate
func menuWillOpen(menu: NSMenu) {
menuVisible = true
needsDisplay = true
}
func menuDidClose(menu: NSMenu) {
menuVisible = false
menu.delegate = nil
needsDisplay = true
}
//MARK: - DrawRect
override func drawRect(dirtyRect: NSRect) {
statusItem.drawStatusBarBackgroundInRect(bounds, withHighlight:menuVisible)
let origin = NSMakePoint(2.0, 3.0) // adjust origin if necessary
image?.drawAtPoint(origin, fromRect: dirtyRect, operation: .CompositeSourceOver, fraction: 1.0)
}
}
In AppDelegate you need a reference to the custom menu and an instance variable for the NSStatusItem instance
#IBOutlet weak var menu : NSMenu!
var statusItem : NSStatusItem!
In applicationDidFinishLaunching create the view and attach it to the status item. Be aware to set the image of the view after attaching it to make sure the width is considered.
func applicationDidFinishLaunching(aNotification: NSNotification) {
statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1) // NSVariableStatusItemLength)
let statusItemView = StatusItemView(frame: NSRect(x: 0.0, y: 0.0, width: statusItem.length, height: 22.0))
statusItemView.statusItem = statusItem;
statusItemView.menu = menu
statusItem.view = statusItemView
statusItemView.image = NSImage(named: NSImageNameStatusAvailable)
}
The special case control-click to trigger the right-click function is not implemented.

How would I use the UILongPressGestureRecognizer in my App in Swift Spritekit?

I have a node in the middle of my screen and currently have this code that moves the node to the left with each tap on the left side of the screen and to the right with each tap on the right side of the screen. I want to change this to a long press instead of a tap. How would I do this? Thanks! This is the code for the tap.
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
var touch: UITouch = touches.anyObject() as UITouch
var location = touch.locationInNode(self)
var node = self.nodeAtPoint(location)
var speedOfTouch:CGFloat = 30
//moves to the left and right by touch and plays sound effect by touch.
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if location.x < CGRectGetMidX(self.frame) {
hero.position.x -= speedOfTouch
AudioPlayer.play()
} else {
hero.position.x += speedOfTouch
AudioPlayer.play()
}
This should be very straightforward. Initialize a UILongPressGestureRecognizer in your view's initializer. You can optionally adjust the minimumPressDuration and allowableMovement properties if desired before adding the gesture recognizer to the view.
override init(frame aRect: CGRect) {
// ...
// Create the long press gesture recognizer, and configure it
// to call a method named viewLongPressed: on self
let longPress = UILongPressGestureRecognizer(target: self, action: "viewLongPressed:")
// Optionally configure it - see the documentation for explanations
// longPress.minimumPressDuration = 1.0
// longPress.allowableMovement = 15
// Add the gesture recognizer to this view (self)
addGestureRecognizer(longPress)
// ...
}
Then just implement the callback method:
func viewLongPressed(gestureRecognizer: UIGestureRecognizer) {
// Handle the long press event by examining the passed in
// gesture recognizer
}

Swift Spritekit image resizing itself after presenting a new scene

I am adding a sprite to the first screen in the basic SpriteKit template. However whenever I go to the play scene and return to the menu scene the image is enlarged. After checking size and scale mode everything seems to be normal. I am unsure why this is happening.
This is the code for the menu:
let playButton = SKSpriteNode(imageNamed: "play")
override func didMoveToView(view: SKView)
{
/* Setup your scene here */
//Place button in exact middle of the screen
self.playButton.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
addChild(self.playButton)
self.backgroundColor = UIColor.blueColor()
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent)
{
/* Called when a touch begins */
for touch: AnyObject in touches
{
let location = touch.locationInNode(self)
if self.nodeAtPoint(location) == self.playButton
{
var scene = PlayJumper()
let sKView = self.view as SKView!
sKView.ignoresSiblingOrder = true
scene.scaleMode = .AspectFill
scene.size = sKView.bounds.size
sKView.presentScene(scene)
}
}
}
Screenshot of scene before moving to next scene:
This is the code to return to the menu from the next scene, and the image that is altered after returning to the screen.
import SpriteKit
class PlayJumper : SKScene
{
override func didMoveToView(view: SKView){
self.backgroundColor = UIColor.orangeColor()
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent)
{
for touch: AnyObject in touches
{
endGame()
}
}
func endGame()
{
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene
{
let sKView = self.view as SKView!
sKView.ignoresSiblingOrder = true
scene.size = sKView.bounds.size
scene.scaleMode = .AspectFill
sKView.presentScene(scene)
}
}
}
Here is the image after moving back to the view, it is slightly larger than the previous image.
I need the image to stay the same no matter how many times the user goes from the game to the menu.
I can only think of two things that can cause this:
In your viewController, you may have changed the scene anchorPoint of the menu and when you go back to the menu you do not set it as the same thing its set to in the viewController.
You may want to use .ResizeFill instead of .AspectFill
Everytime I experienced this problem, it was because of one of these.

Resources