web-dev-qa-db-fra.com

Configuration des boutons dans SKScene

Je découvre que UIButtons ne fonctionne pas très bien avec SKScene. Je tente donc de sous-classe SKNode pour créer un bouton dans SpriteKit

J'aimerais que cela fonctionne: si j'initialise un bouton dans SKScene et active les événements tactiles, le bouton appellera une méthode dans ma SKScene lorsqu'il sera enfoncé. 

J'apprécierais tout conseil qui me conduirait à trouver la solution à ce problème. Merci.

76
AlexHeuman

vous pouvez utiliser un SKSpriteNode comme bouton, puis, lorsque l'utilisateur le touche, vérifiez si c'est le noeud touché Utilisez la propriété name du SKSpriteNode pour identifier le nœud:

//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;
}

Ajouter un noeud à votre scène:

[self addChild: [self fireButtonNode]];

Poignées touchent:

//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...
    }
}
102
AndyOS

J'ai créé ma propre classe Button avec laquelle je travaille . 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);
}

Un exemple: pour initialiser un bouton, vous écrivez les lignes suivantes:

    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];

De plus, vous avez besoin de la méthode 'buttonAction' dans votre classe . * Aucune garantie que cette classe fonctionne correctement dans tous les cas. Je suis encore assez nouveau pour objectif-c. *

Si vous pensez que cela est ennuyeux et inutile, vous pouvez désactiver la vérification dans les paramètres de construction en définissant 'Activer la vérification stricte de objc_msgSend Calls' sur' No '

52
dennis-tra

Pour les personnes qui écrivent leurs jeux dans Swift! J'espère que ça aide:

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)
        }
    }

}
18
Groot

Si vous le souhaitez, vous pouvez utiliser UIButton (ou tout autre UIView).

Quand une SKScene est créée, elle n'existe pas encore dans une SKView. Vous devez implémenter didMoveToView: sur votre sous-classe SKScene. À ce stade, vous avez accès à la variable SKView dans laquelle la scène est placée et vous pouvez y ajouter des objets UIKit. Pour la beauté, je les ai effacés…

- (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) {
                     ;
                   }];
}

vous devrez les retirer délibérément de la scène lorsque vous vous retirez, à moins que cela ne soit tout à fait logique de rester là.

- (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];
                 }];
  }
}

Pour ceux qui ne connaissent pas la création par le programme d'une UIButton, voici un exemple (vous pouvez faire 100 choses différemment ici)…

- (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;
}

Rappel: UIView L’origine est en haut à gauche, SKScene L’origine est en bas à gauche.

5
bshirley

J'ai utilisé la classe SKButton par Graf .

J'utilise le SKButton pour faire de la navigation de scène. C'est-à-dire qu'une autre scène se présente lorsque l'utilisateur appuie sur le bouton SKButton. J'ai l'erreur EXC_BAD_ACCESS à touchesEnded->[self setIsSelected:NO]. Cela se produit particulièrement souvent sur le dernier iPad doté d'un processeur rapide.

Après vérification et dépannage, je me suis rendu compte que l'objet SKButton est déjà "désalloué" lorsque la fonction setIsSelected est appelée. En effet, j'utilise le SKButton pour naviguer vers la scène suivante, ce qui signifie également que la scène en cours peut être désallouée à tout moment.

J'ai fait une petite modification en plaçant setIsSelected dans la partie "else" comme suit.

J'espère que cela aidera les autres développeurs qui voient également la même erreur.

(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);
}
3
user3204765

Edit: J'ai créé un repo github pour mon SKButtonNode que je garderai, espérons-le, à jour et à jour à mesure que Swift évolue!

SKButtonNode


Malheureusement, je ne peux pas encore commenter la mise en œuvre de SKButton dans Swift par Swift. Super content d'avoir fait ça à Swift! Mais j'ai remarqué qu'il n'incluait pas de fonction pour ajouter du texte au bouton. C’est une fonctionnalité très importante pour moi, de sorte que vous n’avez pas à créer d’actifs distincts pour chaque bouton, mais uniquement en arrière-plan et ajouter du texte dynamique. 

J'ai ajouté une fonction simple pour ajouter une étiquette de texte à SKButton. Ce n'est probablement pas parfait - je suis nouveau à Swift comme tout le monde! N'hésitez pas à commenter et à m'aider à mettre à jour le mieux possible. J'espère que vous aimez!

 //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        
     } 

Exemple de création de bouton:

    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);

Classe complète listée ci-dessous:

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)
    }
}

}

2
Morgan Wesemann

Quelles excellentes solutions à ce problème! Pour les défonceurs hardcore qui arrivent jusque-là, vous serez comblés! J'ai sous-classé SKScene, et il faut UN appel de fonction pour enregistrer TOUT nœud pour agir comme un UIButton! Voici la classe:

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
    }
}
//------------------------------------------------------------------------------------

}

Il inclut beaucoup d'idées que je n'ai pas encore implémentées et quelques explications sur le code, mais il suffit de le copier-coller dans votre projet pour que vous puissiez l'utiliser tel quel dans votre propre scène. Voici un exemple complet d'utilisation:

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)
}
}

La seule réserve est que si vous implémentez touchesBegan, touchesMoved, touchesEnded et/ou touchesCancelled, vous DEVEZ APPELER SUPER! Ou alors ça ne marchera pas.

Et s'il vous plaît, réalisez que dans cet exemple, il n'y a qu'une SEULE LIGNE DE CODE dont vous avez besoin pour donner à N'IMPORTE QUELLE NŒUD les caractéristiques UIButton! C'était cette ligne:

addButton(playButton, withCompletionHandler: playButtonPushed)

Je suis toujours ouvert aux idées et suggestions. Laissez-les dans les commentaires et Happy Coding !!

Oups, j'ai oublié de mentionner que j'utilise cette extension astucieuse. Vous pouvez le retirer d'une extension (comme vous n'en avez probablement pas besoin dans chaque nœud) et le placer dans ma classe. Je ne l'utilise que dans un seul endroit.

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
}

}

2
mogelbuster

Voici une autre version basée sur le code Swift de Filip. Je viens de le simplifier un peu et de lui permettre de prendre des blocs plutôt que des sélecteurs:

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)
     }
 }
}

Utilisez-le comme ceci:

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

J'espère que cela t'aides.

2
Guillaume Laurent

Ma solution pour résoudre ce problème écrit complètement en Swift, en utilisant des fermetures.

C'est assez simple à utiliser! 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..") }
}
1
txaidw

J'ai créé une classe pour utiliser SKSpriteNode en tant que bouton il y a longtemps. Vous pouvez le trouver sur GitHub ici.

AGSpriteButton

Son implémentation est basée sur UIButton, donc si vous êtes déjà familiarisé avec iOS, vous devriez trouver qu'il est facile de travailler avec.

Vous pouvez également lui attribuer un bloc ou une action SKAction à exécuter lorsque vous appuyez sur le bouton.

Il inclut également une méthode pour configurer une étiquette.

Un bouton sera typiquement déclaré comme ceci:

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];

Et c'est tout. Vous êtes prêt à partir.

0
ZeMoon

Comme je n’étais convaincu d’aucune des options ci-dessus, j’ai créé ma propre solution à partir du dernier Swift4.

0
Bersaelor

Voici un simple bouton écrit avec Swift moderne (4.1.2)

Caractéristiques

  • il accepte 2 noms d'image, 1 pour l'état par défaut et un pour l'état actif
  • le développeur peut définir les fermetures touchBeganCallback et touchEndedCallback pour ajouter un comportement personnalisé 

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?()
    }
}

Comment l'utiliser

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)

    }
}
0
Luca Angeletti

En fait, cela fonctionne bien sur Swift 2.2 sur Xcode 7.3

J'aime bien FTButtonNode ( richy486/FTButtonNode.Swift ) mais il n'est pas possible de spécifier une autre taille (plutôt que la taille de texture par défaut) directement lors de l'initialisation.

Vous devez copier cela avec la méthode d'initialisation personnalisée officielle (semblable à celle-ci) pour que vous puissiez utiliser une autre méthode d'initialisation:

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)

    }

Une autre chose importante est le "temps de sélection". Dans les nouveaux appareils (iPhone 6), le temps entre touchesBegan et touchesEnded est parfois trop rapide et vous ne voyez pas les changements entre defaultTexture et selectedTexture.

Avec cette fonction:

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

vous pouvez réécrire la méthode touchesEnded pour afficher correctement la variation de texture:

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)
        }
}
0
Alessandro Ornano

Et comme nous ne ciblons pas tous iOS, voici le début d'un code que j'ai écrit pour gérer les interactions de la souris sur le Mac.

Question pour les gourous: MacOS propose-t-il des événements tactiles lors de l'utilisation d'un trackpad? Ou sont-ils envoyés dans SpriteKit en tant qu'événements de souris?

Autre question pour les gourous, cette classe ne devrait-elle pas s’appeler correctement SKButton Node ?

Quoi qu'il en soit, essayez ceci ...

#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
0
Maury Markowitz

Malheureusement, SpriteKit n’a pas de nœud de bouton, je ne sais pas pourquoi, car c’est un contrôle très utile. J'ai donc décidé de créer le mien et de le partager via CocoaPods, merci de l’utiliser OOButtonNode . Les boutons peuvent utiliser du texte/fond ou des images, écrites en Swift 4.

0
Oleg O

J'ai sous-classé la classe SKScene et résolu le problème de la résolution des taps dans ce projet.

https://github.com/Prasad9/SpriteKitButton

Dans celui-ci, tous les nœuds qu'il est nécessaire de connaître lors de la mise sur écoute doivent être nommés. 

En plus de détecter le toucher du bouton, ce projet vous permet également de détecter si le contact sur un nœud particulier a commencé ou s'est terminé.

Pour obtenir une action de tapotement, remplacez la méthode suivante dans votre fichier Scene.

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

Pour connaître le début du contact sur un corps particulier, remplacez la méthode suivante dans votre fichier Scene.

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

Pour connaître la fin du contact sur un corps particulier, remplacez la méthode suivante dans votre fichier Scene.

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

La solution de Graf a un problème. Par exemple:

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

[_hudLayer addChild:_pauseButton];

_hudLayer est un SKNode, une propriété de ma scène. Donc, vous obtiendrez une exception, à cause de la méthode touchesEnded dans SKButton. Il appellera [SKSpriteNode pauseButtonPressed], pas avec scène.

La solution pour changer soi-même. Pour toucher une cible:

- (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];
}}
0
kartpick