15

I'm making a game and I want to have a button. How do I handle tapping it? I use separate class for UI, it is SKSpriteNode that holds all buttons and interface elements, and I don't want Scene to handle those button presses for me in touches began method.

As I know we can check for node that is being touched in touches began, so to implement regular button with touch up inside I need to write code in touchesBegan and touchesEnded, this looks like a bit overkill.

Or should I use regular UIButton? But I know that you can't add those dynamically, only in didMoveToView method, and this looks bad.

Kristian
  • 21,204
  • 19
  • 101
  • 176
Dvole
  • 5,725
  • 10
  • 54
  • 87
  • You'll have to use touchesBegan/Ended, how else will you get touch input? One way of doing it: https://github.com/KoboldKit/KoboldKit/blob/master/KoboldKit/KoboldKitFree/Framework/Behaviors/UserInterface/KKButtonBehavior.m – CodeSmile Nov 15 '13 at 21:55
  • my toggle button example http://stackoverflow.com/questions/19688451/how-to-make-a-toggle-button-on-spritekit/19694729#19694729 – DogCoffee Nov 15 '13 at 23:58
  • what looks bad for you in adding UIButton in didMoveToView? If it shows a bit latter than scene loads, try setting "pausesIncomingScene" or SKTransition that you use for showing the SKScene..I personaly, use SKSpriteNode with touches began method... – BSevo Nov 17 '13 at 20:22

2 Answers2

11

I found this online a while ago, courtesy of a member who goes by the name of Graf, this works nicely for my problems.

#import <SpriteKit/SpriteKit.h>
@interface SKBButtonNode : 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;

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

- (instancetype)initWithImageNamedNormal:(NSString *)normal selected:(NSString *)selected;
- (instancetype)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

And the implementation

//
//
//  Courtesy of Graf on Stack Overflow
//
//
//
#import "SKBButtonNode.h"



@implementation SKBButtonNode

#pragma mark Texture Initializer

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

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

/**
 * This is the designated Initializer
 */
- (instancetype)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

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

- (instancetype)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]) {
        if (_actionTouchDown){
            [self.parent performSelectorOnMainThread:_actionTouchDown withObject:_targetTouchDown waitUntilDone:YES];
            [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)) {
        if (_actionTouchUpInside){
            [self.parent performSelectorOnMainThread:_actionTouchUpInside withObject:_targetTouchUpInside waitUntilDone:YES];
        }
    }
    [self setIsSelected:NO];
        if (_actionTouchUp){
        [self.parent performSelectorOnMainThread:_actionTouchUp withObject:_targetTouchUp waitUntilDone:YES];
    }
}
@end
Andrew97p
  • 565
  • 6
  • 11
  • 1
    Would love to give you a -1 for every use of `objc_msgSend`. – Sulthan Feb 17 '14 at 14:13
  • There, replaced all the `objc_msgSend()` – Andrew97p Feb 17 '14 at 14:26
  • @Andrew97p, can you explain please, why do we need to perform selector on self.parent, but not just self? – AndrewShmig Mar 09 '14 at 10:01
  • 1
    @AndrewShmig I don't really need to, I do because I add my buttons to the scene, so when I use sel.parent I am telling the scene to perform the selector, which just made it easier for me while developing my app to see which scene called which methods. It really doesn't matter, since you are saying perform selector on a given target, so if you want to change it when you use it by all means use self instead. – Andrew97p Mar 09 '14 at 13:58
  • The modified code makes a bad assumption that can lead to a crash. Self.parent might not be the target if for example you add the button to another SKNode that is in your SKScene but the SKScene is the target. if ([_targetTouchUpInside respondsToSelector:_actionTouchUpInside]) { if (_actionTouchUpInside){ [_targetTouchUpInside performSelectorOnMainThread:_actionTouchUpInside withObject:_targetTouchUpInside waitUntilDone:YES]; } } – Kenrik March Mar 15 '14 at 07:27
  • @Andrew97p I wrote the original code [here](http://stackoverflow.com/questions/19082202/setting-up-buttons-in-skscene/19199748#19199748) and I'd already implement the possibility to use SKTexture instances as well as NSString's, so it doesn't have some advanced functionality over the original (you only changed the return type from "id" to "instancetype"). – dennis-tra Apr 22 '14 at 20:53
  • 1
    @Graf I'm sorry, you are correct. In the version of the code I provided with this answer, I did only change the return type and replace the objc_msgSend() calls. I made a mistake when I said I had altered it to accept textures. In a different version of the code that I did not share, I made it accept UIImages as well, and was in a hurry when I posted and must have made a mistake in what I said. I'm sorry and will edit my answer appropriately. – Andrew97p Apr 23 '14 at 00:14
  • This solution does not work for me. I have copied it exactly, yet I always get an Exception in the implementation file stating that `[self setNormalTexture:normal];' is an unrecognized selector. – Micrified Feb 22 '15 at 21:23
  • Can someone show example code on how to get this class working? I tried what seems the usual way (SKBButtonNode *button1 = [[SKBButton alloc] init];) and added the details such as size, etc.. But it doesn't display for some reason after the whole [self addChild:button1]; – Krekin Jun 10 '15 at 08:34
8

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 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.

EDIT: Since posting this answer, a few enhancements have been made to AGSpriteButton.

You can now assign blocks to be executed on touch events:

[button performBlock:^{
        [self addSpaceshipAtPoint:[NSValue valueWithCGPoint:CGPointMake(100, 100)]];
    } onEvent:AGButtonControlEventTouchUp];

Also, an SKAction object to be performed on an instance of SKNode (or any of it's subclasses) can be assigned:

[button addTarget:self 
        selector:@selector(addSpaceshipAtPoint:) 
        withObject:[NSValue valueWithCGPoint:CGPointMake(self.size.width / 2, self.size.height / 2)]         
        forControlEvent:AGButtonControlEventTouchUpInside];

AGSpriteButton can be used with Swift as well!

let button = AGSpriteButton(color: UIColor.greenColor(), andSize: CGSize(width: 200, height: 60))
button.position = CGPoint(x: self.size.width / 2, y: self.size.height / 2)
button.addTarget(self, selector: "addSpaceship", withObject: nil, forControlEvent:AGButtonControlEvent.TouchUpInside)
button.setLabelWithText("Spaceship", andFont: nil, withColor: UIColor.blackColor())
addChild(button)
ZeMoon
  • 20,054
  • 5
  • 57
  • 98