Setting up buttons in SKScene

I'm discovering that UIButtons don't work very well with SKScene, So I'm attempting to subclass SKNode to make a button in SpriteKit.

The way I would like it to work is that if I initialize a button in SKScene and enable touch events, then the button will call a method in my SKScene when it is pressed.

I'd appreciate any advice that would lead me to finding the solution to this problem. Thanks.

51749 次浏览

you could use a SKSpriteNode as your button, and then when the user touches, check if that was the node touched. Use the SKSpriteNode's name property to identify the node:

//fire button
- (SKSpriteNode *)fireButtonNode
{
SKSpriteNode *fireNode = [SKSpriteNode spriteNodeWithImageNamed:@"fireButton.png"];
fireNode.position = CGPointMake(fireButtonX,fireButtonY);
fireNode.name = @"fireButtonNode";//how the node is identified later
fireNode.zPosition = 1.0;
return fireNode;
}

Add node to your scene:

[self addChild: [self fireButtonNode]];

Handle touches:

//handle touch events
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode *node = [self nodeAtPoint:location];


//if fire button touched, bring the rain
if ([node.name isEqualToString:@"fireButtonNode"]) {
//do whatever...
}
}

I've made my own Button-Class that I'm working with. SKButton.h:

#import <SpriteKit/SpriteKit.h>
@interface SKButton : SKSpriteNode


@property (nonatomic, readonly) SEL actionTouchUpInside;
@property (nonatomic, readonly) SEL actionTouchDown;
@property (nonatomic, readonly) SEL actionTouchUp;
@property (nonatomic, readonly, weak) id targetTouchUpInside;
@property (nonatomic, readonly, weak) id targetTouchDown;
@property (nonatomic, readonly, weak) id targetTouchUp;


@property (nonatomic) BOOL isEnabled;
@property (nonatomic) BOOL isSelected;
@property (nonatomic, readonly, strong) SKLabelNode *title;
@property (nonatomic, readwrite, strong) SKTexture *normalTexture;
@property (nonatomic, readwrite, strong) SKTexture *selectedTexture;
@property (nonatomic, readwrite, strong) SKTexture *disabledTexture;


- (id)initWithTextureNormal:(SKTexture *)normal selected:(SKTexture *)selected;
- (id)initWithTextureNormal:(SKTexture *)normal selected:(SKTexture *)selected disabled:(SKTexture *)disabled; // Designated Initializer


- (id)initWithImageNamedNormal:(NSString *)normal selected:(NSString *)selected;
- (id)initWithImageNamedNormal:(NSString *)normal selected:(NSString *)selected disabled:(NSString *)disabled;


/** Sets the target-action pair, that is called when the Button is tapped.
"target" won't be retained.
*/
- (void)setTouchUpInsideTarget:(id)target action:(SEL)action;
- (void)setTouchDownTarget:(id)target action:(SEL)action;
- (void)setTouchUpTarget:(id)target action:(SEL)action;


@end

SKButton.m:

#import "SKButton.h"
#import <objc/message.h>




@implementation SKButton


#pragma mark Texture Initializer


/**
* Override the super-classes designated initializer, to get a properly set SKButton in every case
*/
- (id)initWithTexture:(SKTexture *)texture color:(UIColor *)color size:(CGSize)size {
return [self initWithTextureNormal:texture selected:nil disabled:nil];
}


- (id)initWithTextureNormal:(SKTexture *)normal selected:(SKTexture *)selected {
return [self initWithTextureNormal:normal selected:selected disabled:nil];
}


/**
* This is the designated Initializer
*/
- (id)initWithTextureNormal:(SKTexture *)normal selected:(SKTexture *)selected disabled:(SKTexture *)disabled {
self = [super initWithTexture:normal color:[UIColor whiteColor] size:normal.size];
if (self) {
[self setNormalTexture:normal];
[self setSelectedTexture:selected];
[self setDisabledTexture:disabled];
[self setIsEnabled:YES];
[self setIsSelected:NO];


_title = [SKLabelNode labelNodeWithFontNamed:@"Arial"];
[_title setVerticalAlignmentMode:SKLabelVerticalAlignmentModeCenter];
[_title setHorizontalAlignmentMode:SKLabelHorizontalAlignmentModeCenter];


[self addChild:_title];
[self setUserInteractionEnabled:YES];
}
return self;
}


#pragma mark Image Initializer


- (id)initWithImageNamedNormal:(NSString *)normal selected:(NSString *)selected {
return [self initWithImageNamedNormal:normal selected:selected disabled:nil];
}


- (id)initWithImageNamedNormal:(NSString *)normal selected:(NSString *)selected disabled:(NSString *)disabled {
SKTexture *textureNormal = nil;
if (normal) {
textureNormal = [SKTexture textureWithImageNamed:normal];
}


SKTexture *textureSelected = nil;
if (selected) {
textureSelected = [SKTexture textureWithImageNamed:selected];
}


SKTexture *textureDisabled = nil;
if (disabled) {
textureDisabled = [SKTexture textureWithImageNamed:disabled];
}


return [self initWithTextureNormal:textureNormal selected:textureSelected disabled:textureDisabled];
}








#pragma -
#pragma mark Setting Target-Action pairs


- (void)setTouchUpInsideTarget:(id)target action:(SEL)action {
_targetTouchUpInside = target;
_actionTouchUpInside = action;
}


- (void)setTouchDownTarget:(id)target action:(SEL)action {
_targetTouchDown = target;
_actionTouchDown = action;
}


- (void)setTouchUpTarget:(id)target action:(SEL)action {
_targetTouchUp = target;
_actionTouchUp = action;
}


#pragma -
#pragma mark Setter overrides


- (void)setIsEnabled:(BOOL)isEnabled {
_isEnabled = isEnabled;
if ([self disabledTexture]) {
if (!_isEnabled) {
[self setTexture:_disabledTexture];
} else {
[self setTexture:_normalTexture];
}
}
}


- (void)setIsSelected:(BOOL)isSelected {
_isSelected = isSelected;
if ([self selectedTexture] && [self isEnabled]) {
if (_isSelected) {
[self setTexture:_selectedTexture];
} else {
[self setTexture:_normalTexture];
}
}
}


#pragma -
#pragma mark Touch Handling


/**
* This method only occurs, if the touch was inside this node. Furthermore if
* the Button is enabled, the texture should change to "selectedTexture".
*/
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self isEnabled]) {
objc_msgSend(_targetTouchDown, _actionTouchDown);
[self setIsSelected:YES];
}
}


/**
* If the Button is enabled: This method looks, where the touch was moved to.
* If the touch moves outside of the button, the isSelected property is restored
* to NO and the texture changes to "normalTexture".
*/
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self isEnabled]) {
UITouch *touch = [touches anyObject];
CGPoint touchPoint = [touch locationInNode:self.parent];


if (CGRectContainsPoint(self.frame, touchPoint)) {
[self setIsSelected:YES];
} else {
[self setIsSelected:NO];
}
}
}


/**
* If the Button is enabled AND the touch ended in the buttons frame, the
* selector of the target is run.
*/
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint touchPoint = [touch locationInNode:self.parent];


if ([self isEnabled] && CGRectContainsPoint(self.frame, touchPoint)) {
objc_msgSend(_targetTouchUpInside, _actionTouchUpInside);
}
[self setIsSelected:NO];
objc_msgSend(_targetTouchUp, _actionTouchUp);
}

An example: To initialize a button, you write the following lines:

    SKButton *backButton = [[SKButton alloc] initWithImageNamedNormal:@"buttonNormal" selected:@"buttonSelected"];
[backButton setPosition:CGPointMake(100, 100)];
[backButton.title setText:@"Button"];
[backButton.title setFontName:@"Chalkduster"];
[backButton.title setFontSize:20.0];
[backButton setTouchUpInsideTarget:self action:@selector(buttonAction)];
[self addChild:backButton];

Furthermore you need the 'buttonAction' method in your class. * No warranty that this class is working right in every case. I'm still quite new to objective-c. *

If you think having to do this is annoying and pointless you can disable the check in the build settings by setting 'Enable strict checking of objc_msgSend Calls' to 'No'

I have used SKButton class by Graf.

I use the SKButton to do scene navigation. i.e present another scene when the user press the SKButton. I get EXC_BAD_ACCESS error at touchesEnded->[self setIsSelected:NO]. This happens especially frequently on the latest iPad with fast CPU.

After checking and troubleshooting, I realised that the SKButton object is already "deallocated" when the setIsSelected function is being called. This is because I use the SKButton to navigate to next scene and this also means that the current scene can be deallocated any time.

I made a small change by putting the setIsSelected in the "else" portion as follows.

Hope this helps for other developer who also see the same error.

(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint touchPoint = [touch locationInNode:self.parent];


if ([self isEnabled] && CGRectContainsPoint(self.frame, touchPoint)) {
objc_msgSend(_targetTouchUpInside, _actionTouchUpInside);
} else {
[self setIsSelected:NO];
}
objc_msgSend(_targetTouchUp, _actionTouchUp);
}

I had created a class for using SKSpriteNode as a button quite a while ago. You can find it on GitHub here.

AGSpriteButton

It's implementation is based on UIButton, so if you are already familiar with iOS, you should find it easy to work with.

It can also be assigned a block or an SKAction to be executed when the button is pressed.

It includes a method to set up a label as well.

A button will typically be declared like so:

AGSpriteButton *button = [AGSpriteButton buttonWithColor:[UIColor redColor] andSize:CGSizeMake(300, 100)];
[button setLabelWithText:@"Button Text" andFont:nil withColor:nil];
button.position = CGPointMake(self.size.width / 2, self.size.height / 3);
[button addTarget:self selector:@selector(someSelector) withObject:nil forControlEvent:AGButtonControlEventTouchUpInside];
[self addChild:button];

And that's it. You're good to go.

For people writing their games in Swift! I have rewritten the essential parts of Graf's solution to a swift class. Hope it helps:

import Foundation
import SpriteKit


class FTButtonNode: SKSpriteNode {


enum FTButtonActionType: Int {
case TouchUpInside = 1,
TouchDown, TouchUp
}


var isEnabled: Bool = true {
didSet {
if (disabledTexture != nil) {
texture = isEnabled ? defaultTexture : disabledTexture
}
}
}
var isSelected: Bool = false {
didSet {
texture = isSelected ? selectedTexture : defaultTexture
}
}
var defaultTexture: SKTexture
var selectedTexture: SKTexture


required init(coder: NSCoder) {
fatalError("NSCoding not supported")
}


init(normalTexture defaultTexture: SKTexture!, selectedTexture:SKTexture!, disabledTexture: SKTexture?) {


self.defaultTexture = defaultTexture
self.selectedTexture = selectedTexture
self.disabledTexture = disabledTexture


super.init(texture: defaultTexture, color: UIColor.whiteColor(), size: defaultTexture.size())


userInteractionEnabled = true


// Adding this node as an empty layer. Without it the touch functions are not being called
// The reason for this is unknown when this was implemented...?
let bugFixLayerNode = SKSpriteNode(texture: nil, color: nil, size: defaultTexture.size())
bugFixLayerNode.position = self.position
addChild(bugFixLayerNode)


}


/**
* Taking a target object and adding an action that is triggered by a button event.
*/
func setButtonAction(target: AnyObject, triggerEvent event:FTButtonActionType, action:Selector) {


switch (event) {
case .TouchUpInside:
targetTouchUpInside = target
actionTouchUpInside = action
case .TouchDown:
targetTouchDown = target
actionTouchDown = action
case .TouchUp:
targetTouchUp = target
actionTouchUp = action
}


}


var disabledTexture: SKTexture?
var actionTouchUpInside: Selector?
var actionTouchUp: Selector?
var actionTouchDown: Selector?
weak var targetTouchUpInside: AnyObject?
weak var targetTouchUp: AnyObject?
weak var targetTouchDown: AnyObject?


override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!)  {
let touch: AnyObject! = touches.anyObject()
let touchLocation = touch.locationInNode(parent)


if (!isEnabled) {
return
}
isSelected = true
if (targetTouchDown != nil && targetTouchDown!.respondsToSelector(actionTouchDown!)) {
UIApplication.sharedApplication().sendAction(actionTouchDown!, to: targetTouchDown, from: self, forEvent: nil)
}




}


override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!)  {


if (!isEnabled) {
return
}


let touch: AnyObject! = touches.anyObject()
let touchLocation = touch.locationInNode(parent)


if (CGRectContainsPoint(frame, touchLocation)) {
isSelected = true
} else {
isSelected = false
}


}


override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {


if (!isEnabled) {
return
}


isSelected = false


if (targetTouchUpInside != nil && targetTouchUpInside!.respondsToSelector(actionTouchUpInside!)) {
let touch: AnyObject! = touches.anyObject()
let touchLocation = touch.locationInNode(parent)


if (CGRectContainsPoint(frame, touchLocation) ) {
UIApplication.sharedApplication().sendAction(actionTouchUpInside!, to: targetTouchUpInside, from: self, forEvent: nil)
}


}


if (targetTouchUp != nil && targetTouchUp!.respondsToSelector(actionTouchUp!)) {
UIApplication.sharedApplication().sendAction(actionTouchUp!, to: targetTouchUp, from: self, forEvent: nil)
}
}


}

If you desire, you can use UIButton (or any other UIView).

When a SKScene is created, it doesn't yet exist in an SKView. You should implement didMoveToView: on your SKScene subclass. At this point, you have access to the SKView the scene is placed in and you can add UIKit objects to it. For prettiness, I faded them in …

- (void)didMoveToView:(SKView *)view {
UIView *b = [self _createButton];  // <-- performs [self.view addSubview:button]
// create other UI elements, also add them to the list to remove …
self.customSubviews = @[b];


b.alpha = 0;


[UIView animateWithDuration:0.4
delay:2.4
options:UIViewAnimationOptionCurveEaseIn
animations:^{
b.alpha = 1;
} completion:^(BOOL finished) {
;
}];
}

you will need to deliberately remove them from the scene when you transition away, unless of course it makes total sense for them to remain there.

- (void)removeCustomSubviews {
for (UIView *v in self.customSubviews) {
[UIView animateWithDuration:0.2
delay:0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
v.alpha = 0;
} completion:^(BOOL finished) {
[v removeFromSuperview];
}];
}
}

For those unfamiliar with programmatically creating a UIButton, here one example (you could do a 100 things differently here) …

- (UIButton *)_createButton {
UIButton *b = [UIButton buttonWithType:UIButtonTypeCustom];
[b setTitle:@"Continue" forState:UIControlStateNormal];
[b setBackgroundImage:[UIImage imageNamed:@"GreenButton"] forState:UIControlStateNormal];
[b setBackgroundImage:[UIImage imageNamed:@"GreenButtonSelected"] forState:UIControlStateHighlighted];
b.titleLabel.adjustsFontSizeToFitWidth = YES;
b.titleLabel.font = [UIFont fontWithName:@"HelveticaNeue-Bold" size:36];
b.frame = CGRectMake(self.size.width * .7, self.size.height * .2, self.size.width * .2, self.size.height * .1);
[b addTarget:self action:@selector(continuePlay) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:b];


return b;
}

Reminder: UIView origin is in the upper left, SKScene origin is in the lower left.

Edit: I've made a github repo for my SKButtonNode that I'll hopefully be keeping current and updating as swift evolves!

SKButtonNode


Unfortunately I cannot comment yet on Filip's swift implementation of SKButton in Swift. Super happy that he made this in Swift! But, I noticed that he didn't include a function to add text to the button. This is a huge feature to me, so that you don't have to create separate assets for every single button, rather just the background and add dynamic text.

I added a simple function to add a text label to SKButton. It likely isn't perfect--I'm new to Swift just like everyone else! Feel free to comment and help me update this to the best it can be. Hope you guys like!

 //Define label with the textures
var defaultTexture: SKTexture
var selectedTexture: SKTexture


//New defining of label
var label: SKLabelNode


//Updated init() function:


init(normalTexture defaultTexture: SKTexture!, selectedTexture:SKTexture!, disabledTexture: SKTexture?) {


self.defaultTexture = defaultTexture
self.selectedTexture = selectedTexture
self.disabledTexture = disabledTexture


//New initialization of label
self.label = SKLabelNode(fontNamed: "Helvetica");


super.init(texture: defaultTexture, color: UIColor.whiteColor(), size: defaultTexture.size())
userInteractionEnabled = true


//Creating and adding a blank label, centered on the button
self.label.verticalAlignmentMode = SKLabelVerticalAlignmentMode.Center;
self.label.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.Center;
addChild(self.label)


// Adding this node as an empty layer. Without it the touch functions are not being called
// The reason for this is unknown when this was implemented...?
let bugFixLayerNode = SKSpriteNode(texture: nil, color: nil, size: defaultTexture.size())
bugFixLayerNode.position = self.position
addChild(bugFixLayerNode)


}








/*
New function for setting text. Calling function multiple times does
not create a ton of new labels, just updates existing label.
You can set the title, font type and font size with this function
*/


func setButtonLabel(#title: NSString, font: String, fontSize: CGFloat) {
var title = title
var font = font
var fontSize = fontSize


self.label.text = title
self.label.fontSize = fontSize
self.label.fontName = font
}

Sample creation of button:

    var buttonTexture = SKTexture(imageNamed: "Button");
var buttonPressedTexture = SKTexture(imageNamed: "Button Pressed");
var button = SKButton(normalTexture:buttonTexture, selectedTexture:buttonPressedTexture, disabledTexture:buttonPressedTexture);
button.setButtonLabel(title: "Play",font: "Helvetica",fontSize: 40);
button.position = CGPointMake(self.frame.size.width/2, self.frame.size.height/2);
self.addChild(button);

Full Class Listed Below:

import Foundation
import SpriteKit




class SKButton: SKSpriteNode {








enum FTButtonActionType: Int {
case TouchUpInside = 1,
TouchDown, TouchUp
}


var isEnabled: Bool = true {
didSet {
if (disabledTexture != nil) {
texture = isEnabled ? defaultTexture : disabledTexture
}
}
}
var isSelected: Bool = false {
didSet {
texture = isSelected ? selectedTexture : defaultTexture
}
}
var defaultTexture: SKTexture
var selectedTexture: SKTexture
var label: SKLabelNode




required init(coder: NSCoder) {
fatalError("NSCoding not supported")
}


init(normalTexture defaultTexture: SKTexture!, selectedTexture:SKTexture!, disabledTexture: SKTexture?) {


self.defaultTexture = defaultTexture
self.selectedTexture = selectedTexture
self.disabledTexture = disabledTexture
self.label = SKLabelNode(fontNamed: "Helvetica");
super.init(texture: defaultTexture, color: UIColor.whiteColor(), size: defaultTexture.size())
userInteractionEnabled = true




self.label.verticalAlignmentMode = SKLabelVerticalAlignmentMode.Center;
self.label.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.Center;
addChild(self.label)


// Adding this node as an empty layer. Without it the touch functions are not being called
// The reason for this is unknown when this was implemented...?
let bugFixLayerNode = SKSpriteNode(texture: nil, color: nil, size: defaultTexture.size())
bugFixLayerNode.position = self.position
addChild(bugFixLayerNode)


}


/**
* Taking a target object and adding an action that is triggered by a button event.
*/
func setButtonAction(target: AnyObject, triggerEvent event:FTButtonActionType, action:Selector) {


switch (event) {
case .TouchUpInside:
targetTouchUpInside = target
actionTouchUpInside = action
case .TouchDown:
targetTouchDown = target
actionTouchDown = action
case .TouchUp:
targetTouchUp = target
actionTouchUp = action
}


}




func setButtonLabel(#title: NSString, font: String, fontSize: CGFloat) {
var title = title;
var font = font;
var fontSize = fontSize;


self.label.text = title;
self.label.fontSize = fontSize;
self.label.fontName = font;


}


var disabledTexture: SKTexture?
var actionTouchUpInside: Selector?
var actionTouchUp: Selector?
var actionTouchDown: Selector?
weak var targetTouchUpInside: AnyObject?
weak var targetTouchUp: AnyObject?
weak var targetTouchDown: AnyObject?


override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!)  {
let touch: AnyObject! = touches.anyObject()
let touchLocation = touch.locationInNode(parent)


if (!isEnabled) {
return
}
isSelected = true
if (targetTouchDown != nil && targetTouchDown!.respondsToSelector(actionTouchDown!)) {
UIApplication.sharedApplication().sendAction(actionTouchDown!, to: targetTouchDown, from: self, forEvent: nil)
}




}


override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!)  {


if (!isEnabled) {
return
}


let touch: AnyObject! = touches.anyObject()
let touchLocation = touch.locationInNode(parent)


if (CGRectContainsPoint(frame, touchLocation)) {
isSelected = true
} else {
isSelected = false
}


}


override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {


if (!isEnabled) {
return
}


isSelected = false


if (targetTouchUpInside != nil && targetTouchUpInside!.respondsToSelector(actionTouchUpInside!)) {
let touch: AnyObject! = touches.anyObject()
let touchLocation = touch.locationInNode(parent)


if (CGRectContainsPoint(frame, touchLocation) ) {
UIApplication.sharedApplication().sendAction(actionTouchUpInside!, to: targetTouchUpInside, from: self, forEvent: nil)
}


}


if (targetTouchUp != nil && targetTouchUp!.respondsToSelector(actionTouchUp!)) {
UIApplication.sharedApplication().sendAction(actionTouchUp!, to: targetTouchUp, from: self, forEvent: nil)
}
}

}

Here's another version based on Filip's Swift code. I've just simplified it a little and allowed it to take blocks rather than only selectors :

import Foundation
import SpriteKit


enum FTButtonTarget {
case aSelector(Selector, AnyObject)
case aBlock(() -> Void)
}


class FTButtonNode: SKSpriteNode {


var actionTouchUp : FTButtonTarget?
var actionTouchUpInside : FTButtonTarget?
var actionTouchDown : FTButtonTarget?


var isEnabled: Bool = true {
didSet {
if (disabledTexture != nil) {
texture = isEnabled ? defaultTexture : disabledTexture
}
}
}
var isSelected: Bool = false {
didSet {
texture = isSelected ? selectedTexture : defaultTexture
}
}


var defaultTexture: SKTexture
var selectedTexture: SKTexture


required init(coder: NSCoder) {
fatalError("NSCoding not supported")
}


init(normalTexture defaultTexture: SKTexture!, selectedTexture:SKTexture!, disabledTexture: SKTexture?) {


self.defaultTexture = defaultTexture
self.selectedTexture = selectedTexture
self.disabledTexture = disabledTexture


super.init(texture: defaultTexture, color: UIColor.whiteColor(), size: defaultTexture.size())


userInteractionEnabled = true


// Adding this node as an empty layer. Without it the touch functions are not being called
// The reason for this is unknown when this was implemented...?
let bugFixLayerNode = SKSpriteNode(texture: nil, color: nil, size: defaultTexture.size())
bugFixLayerNode.position = self.position
addChild(bugFixLayerNode)


}


var disabledTexture: SKTexture?


func callTarget(buttonTarget:FTButtonTarget) {


switch buttonTarget {
case let .aSelector(selector, target):
if target.respondsToSelector(selector) {
UIApplication.sharedApplication().sendAction(selector, to: target, from: self, forEvent: nil)
}
case let .aBlock(block):
block()
}


}


override func touchesBegan(touches: NSSet, withEvent event: UIEvent)  {
let touch: AnyObject! = touches.anyObject()
let touchLocation = touch.locationInNode(parent)


if (!isEnabled) {
return
}
isSelected = true


if let act = actionTouchDown {
callTarget(act)
}


}


override func touchesMoved(touches: NSSet, withEvent event: UIEvent)  {


if (!isEnabled) {
return
}


let touch: AnyObject! = touches.anyObject()
let touchLocation = touch.locationInNode(parent)


if (CGRectContainsPoint(frame, touchLocation)) {
isSelected = true
} else {
isSelected = false
}


}


override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {


if (!isEnabled) {
return
}


isSelected = false


let touch: AnyObject! = touches.anyObject()
let touchLocation = touch.locationInNode(parent)


if (CGRectContainsPoint(frame, touchLocation) ) {


if let act = actionTouchUpInside {
callTarget(act)
}
}


if let act = actionTouchUp {
callTarget(act)
}
}
}

Use it like this :

       aFTButton.actionTouchUpInside = FTButtonTarget.aBlock({ () -> Void in
println("button touched")
})

Hope this helps.

And since all of us aren't targeting iOS, here's the start of some code I wrote to handle mouse interaction on the Mac.

Question for the gurus: does MacOS offer touch events when using a trackpad? Or are these sent into SpriteKit as mouse events?

Another question for the gurus, shouldn't this class properly be called SKButtonNode?

Anyway, try this...

#if os(iOS)
override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!)  {
let touch: AnyObject! = touches.anyObject()
let touchLocation = touch.locationInNode(parent)


if (!isEnabled) { return }


isSelected = true
if (targetTouchDown != nil && targetTouchDown!.respondsToSelector(actionTouchDown!)) {
UIApplication.sharedApplication().sendAction(actionTouchDown!, to: targetTouchDown, from: self, forEvent: nil)
}
}


override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!)  {
if (!isEnabled) { return }


let touch: AnyObject! = touches.anyObject()
let touchLocation = touch.locationInNode(parent)


if (CGRectContainsPoint(frame, touchLocation)) {
isSelected = true
} else {
isSelected = false
}
}


override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {
if (!isEnabled) { return }


isSelected = false


if (targetTouchUpInside != nil && targetTouchUpInside!.respondsToSelector(actionTouchUpInside!)) {
let touch: AnyObject! = touches.anyObject()
let touchLocation = touch.locationInNode(parent)


if (CGRectContainsPoint(frame, touchLocation) ) {
UIApplication.sharedApplication().sendAction(actionTouchUpInside!, to: targetTouchUpInside, from: self, forEvent: nil)
}
}


if (targetTouchUp != nil && targetTouchUp!.respondsToSelector(actionTouchUp!)) {
UIApplication.sharedApplication().sendAction(actionTouchUp!, to: targetTouchUp, from: self, forEvent: nil)
}
}
#else


// FIXME: needs support for mouse enter and leave, turning on and off selection


override func mouseDown(event: NSEvent) {
if (!isEnabled) { return }


if (targetTouchDown != nil && targetTouchDown!.respondsToSelector(actionTouchDown!)) {
NSApplication.sharedApplication().sendAction(actionTouchDown!, to: targetTouchDown, from: self)
}
}


override func mouseUp(event: NSEvent) {
if (!isEnabled) { return }


if (targetTouchUpInside != nil && targetTouchUpInside!.respondsToSelector(actionTouchUpInside!)) {
let touchLocation = event.locationInNode(parent)


if (CGRectContainsPoint(frame, touchLocation) ) {
NSApplication.sharedApplication().sendAction(actionTouchUpInside!, to: targetTouchUpInside, from: self)
}
}


if (targetTouchUp != nil && targetTouchUp!.respondsToSelector(actionTouchUp!)) {
NSApplication.sharedApplication().sendAction(actionTouchUp!, to: targetTouchUp, from: self)
}
}
#endif

My solution to solve this problem written completely in SWIFT, using closures.

Its pretty simple to use! https://github.com/txaidw/TWControls

class Test {
var testProperty = "Default String"


init() {
let control = TWButton(normalColor: SKColor.blueColor(), highlightedColor: SKColor.redColor(), size: CGSize(width: 160, height: 80))
control.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
control.position.allStatesLabelText = "PLAY"
control.addClosureFor(.TouchUpInside, target: self, closure: { (scene, sender) -> () in
scene.testProperty = "Changed Property"
})
}


deinit { println("Class Released..") }
}

I have subclassed SKScene class and achieved the problem of solving button taps in this project.

https://github.com/Prasad9/SpriteKitButton

In it, all the nodes which are necessary to be known upon tapped should be named.

In addition to detecting button tap, this project also enables you to detect whether the touch on a particular node has started or ended.

To get tap action, override the following method in your Scene file.

- (void)touchUpInsideOnNodeName:(NSString *)nodeName atPoint:(CGPoint)touchPoint {
// Your code here.
}

To get to know the start of touch on a particular body, override the following method in your Scene file.

 - (void)touchBeginOnNodeName:(NSString *)nodeName {
// Your code here.
}

To get to know the end of touch on a particular body, override the following method in your Scene file.

 - (void)touchEndedOnNodeName:(NSString *)nodeName {
// Your code here.
}

Graf`s solution has one issue. For example:

self.pauseButton = [[AGSKBButtonNode alloc] initWithImageNamed:@"ButtonPause"];
self.pauseButton.position = CGPointMake(0, 0);
[self.pauseButton setTouchUpInsideTarget:self action:@selector(pauseButtonPressed)];


[_hudLayer addChild:_pauseButton];

_hudLayer is a SKNode, a property of my scene. So, you`ll get exception, because of method touchesEnded in SKButton. It will call [SKSpriteNode pauseButtonPressed], not with scene.

The solution to change self.parent to touch target:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint touchPoint = [touch locationInNode:self.parent];


if ([self isEnabled] && CGRectContainsPoint(self.frame, touchPoint)) {
if (_actionTouchUpInside){
[_targetTouchUpInside performSelectorOnMainThread:_actionTouchUpInside withObject:_targetTouchUpInside waitUntilDone:YES];
}
}
[self setIsSelected:NO];
if (_actionTouchUp){
[_targetTouchUp performSelectorOnMainThread:_actionTouchUp withObject:_targetTouchUp waitUntilDone:YES];
}}

Actually this work well on Swift 2.2 on Xcode 7.3

I like FTButtonNode (richy486/FTButtonNode.swift ) but it's not possible to specify another size (rather then default texture size) directly during initialization so I've added this simple method:

You must copy that under the official custom init method (similar to this) so you have another init method to use:

init(normalTexture defaultTexture: SKTexture!, selectedTexture:SKTexture!, disabledTexture: SKTexture?, size:CGSize) {


self.defaultTexture = defaultTexture
self.selectedTexture = selectedTexture
self.disabledTexture = disabledTexture
self.label = SKLabelNode(fontNamed: "Helvetica");


super.init(texture: defaultTexture, color: UIColor.whiteColor(), size: size)
userInteractionEnabled = true


//Creating and adding a blank label, centered on the button
self.label.verticalAlignmentMode = SKLabelVerticalAlignmentMode.Center;
self.label.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.Center;
addChild(self.label)


// Adding this node as an empty layer. Without it the touch functions are not being called
// The reason for this is unknown when this was implemented...?
let bugFixLayerNode = SKSpriteNode(texture: nil, color: UIColor.clearColor(), size: size)
bugFixLayerNode.position = self.position
addChild(bugFixLayerNode)


}

Another important thing is the "selection time", I've seen that in the new devices (iPhone 6) sometime the time between touchesBegan and touchesEnded is too fast and you dont see the changes between defaultTexture and selectedTexture.

With this function:

func dispatchDelay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}

you can re-write the touchesEnded method to show correctly the texture variation:

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if (!isEnabled) {
return
}


dispatchDelay(0.2) {
self.isSelected = false
}


if (targetTouchUpInside != nil && targetTouchUpInside!.respondsToSelector(actionTouchUpInside!)) {
let touch: AnyObject! = touches.first
let touchLocation = touch.locationInNode(parent!)


if (CGRectContainsPoint(frame, touchLocation) ) {
UIApplication.sharedApplication().sendAction(actionTouchUpInside!, to: targetTouchUpInside, from: self, forEvent: nil)
}


}


if (targetTouchUp != nil && targetTouchUp!.respondsToSelector(actionTouchUp!)) {
UIApplication.sharedApplication().sendAction(actionTouchUp!, to: targetTouchUp, from: self, forEvent: nil)
}
}

What a lot of great solutions to this problem! For the hardcore scrollers that make it down this far, you're in for a treat! I have subclassed SKScene, and it takes ONE function call to register ANY node to act like a UIButton! Here is the class:

class KCScene : SKScene {
//------------------------------------------------------------------------------------
//This function is the only thing you use in this class!!!
func addButton(_ node:SKNode, withCompletionHandler handler: @escaping ()->()) {
let data = ButtonData(button: node, actionToPerform: handler)
eligibleButtons.append(data)
}
//------------------------------------------------------------------------------------
private struct ButtonData {
//TODO: make a dictionary with ()->() as the value and SKNode as the key.
//Then refactor this class!
let button:SKNode
let actionToPerform:()->()
}


private struct TouchTrackingData {
//this will be in a dictionary with a UITouch object as the key
let button:SKNode
let originalButtonFrame:CGRect
}


private var eligibleButtons = [ButtonData]()
private var trackedTouches = [UITouch:TouchTrackingData]()
//------------------------------------------------------------------------------------
//TODO: make these functions customizable,
//with these implementations as defaults.
private func applyTouchedDownEffectToNode(node:SKNode) {
node.alpha  = 0.5
node.xScale = 0.8
node.yScale = 0.8
}
private func applyTouchedUpEffectToNode(node:SKNode)   {
node.alpha  = 1
node.xScale = 1
node.yScale = 1
}
//------------------------------------------------------------------------------------
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let touchLocation = touch.location(in: self)
let touchedNode = atPoint(touchLocation)


for buttonData in eligibleButtons {
if touchedNode === buttonData.button {
//then this touch needs to be tracked, as it touched down on an eligible button!
for (t, bD) in trackedTouches {
if bD.button === buttonData.button {
//then this button was already being tracked by a previous touch, disable the previous touch
trackedTouches[t] = nil
}
}
//start tracking this touch
trackedTouches[touch] = TouchTrackingData(button: touchedNode, originalButtonFrame: touchedNode.frameInScene)
applyTouchedDownEffectToNode(node: buttonData.button)
}
}
}
}
//------------------------------------------------------------------------------------
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
if trackedTouches[touch] == nil {continue}
//Now we know this touch is being tracked...
let touchLocation = touch.location(in: self)
//TODO: implement an isBeingTouched property on TouchTrackingData, so
//applyTouchedDown(Up)Effect doesn't have to be called EVERY move the touch makes
if trackedTouches[touch]!.originalButtonFrame.contains(touchLocation) {
//if this tracked touch is touching its button
applyTouchedDownEffectToNode(node: trackedTouches[touch]!.button)
} else {
applyTouchedUpEffectToNode(node: trackedTouches[touch]!.button)
}


}
}
//------------------------------------------------------------------------------------
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
if trackedTouches[touch] == nil {continue}
//Now we know this touch is being tracked...
let touchLocation = touch.location(in: self)


if trackedTouches[touch]!.originalButtonFrame.contains(touchLocation) {
applyTouchedUpEffectToNode(node: trackedTouches[touch]!.button)


for buttonData in eligibleButtons {
if buttonData.button === trackedTouches[touch]!.button {
buttonData.actionToPerform()
}
}
}
trackedTouches[touch] = nil
}
}
//------------------------------------------------------------------------------------
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
for touch in touches! {
if trackedTouches[touch] == nil {continue}
//Now we know this touch is being tracked...
//Since this touch was cancelled, it will not be activating a button,
//and it is not worth checking where the touch was
//we will simply apply the touched up effect regardless and remove the touch from being tracked
applyTouchedUpEffectToNode(node: trackedTouches[touch]!.button)
trackedTouches[touch] = nil
}
}
//------------------------------------------------------------------------------------

}

It includes a lot of ideas I haven't yet implemented and some explanations of the code, but just copy and paste it into your project, and you can use it as-is in your own scene. Here is a complete example usage:

class GameScene : KCScene {
var playButton:SKSpriteNode
override init(size:CGSize) {
playButton = SKSpriteNode(color: SKColor.red, size: CGSize(width:200,height:200))
playButton.position.x = size.width/2
playButton.position.y = size.height*0.75
super.init(size: size)
}
override func didMove(to view: SKView) {
addChild(playButton)
addButton(playButton, withCompletionHandler: playButtonPushed)
}
func playButtonPushed() {
let scene = GameScene(size: CGSize(width: 768, height: 1024))
scene.scaleMode = .aspectFill
view!.presentScene(scene)
}
}

The one caveat, is if you implement touchesBegan, touchesMoved, touchesEnded, and/or touchesCancelled you MUST CALL SUPER! Or else it will not work.

And please realize that in that example, there is really only ONE LINE OF CODE you need to give ANY NODE UIButton characteristics! It was this line:

addButton(playButton, withCompletionHandler: playButtonPushed)

I'm always open for ideas and suggestions. Leave 'em in the comments and Happy Coding!!

Oops, I forgot to mention I use this nifty extension. You can take it out of an extension (as you probably don't need it in every node) and plop it in my class. I only use it in one place.

extension SKNode {
var frameInScene:CGRect {
if let scene = scene, let parent = parent {
let rectOriginInScene = scene.convert(frame.origin, from: parent)
return CGRect(origin: rectOriginInScene, size: frame.size)
}
return frame
}

}

I wasn't convinced of any of the above options, so based on the latest Swift4 I created my own solution.

Unfortunately SpriteKit does not have button node, I do not know why, because it is very useful control. So I decided to create my own and share via CocoaPods, please use it OOButtonNode. Buttons can use text/background or images, written in Swift 4.

Here's a simple button written with modern Swift (4.1.2)

Features

  • it accepts 2 image names, 1 for the default state and one for the active state
  • the developer can set the touchBeganCallback and touchEndedCallback closures to add custom behaviour

Code

import SpriteKit


class SpriteKitButton: SKSpriteNode {


private let textureDefault: SKTexture
private let textureActive: SKTexture


init(defaultImageNamed: String, activeImageNamed:String) {
textureDefault = SKTexture(imageNamed: defaultImageNamed)
textureActive = SKTexture(imageNamed: activeImageNamed)
super.init(texture: textureDefault, color: .clear, size: textureDefault.size())
self.isUserInteractionEnabled = true
}


required init?(coder aDecoder: NSCoder) {
fatalError("Not implemented")
}


var touchBeganCallback: (() -> Void)?
var touchEndedCallback: (() -> Void)?


override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.texture = textureActive
touchBeganCallback?()
}


override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.texture = textureDefault
touchEndedCallback?()
}
}

How to use it

class GameScene: SKScene {


override func didMove(to view: SKView) {


// 1. create the button
let button = SpriteKitButton(defaultImageNamed: "default", activeImageNamed: "active")


// 2. write what should happen when the button is tapped
button.touchBeganCallback = {
print("Touch began")
}


// 3. write what should happen when the button is released
button.touchEndedCallback = {
print("Touch ended")
}


// 4. add the button to the scene
addChild(button)


}
}