Настройка кнопок в SKScene
Я обнаружил, что UIButtons
не очень хорошо работает с SKScene
Итак, я пытаюсь подкласс SKNode
сделать кнопку в SpriteKit
,
Я бы хотел, чтобы это работало так: если я инициализирую кнопку в SKScene
и включить сенсорные события, то кнопка вызовет метод в моем SKScene
когда она нажата.
Буду признателен за любые советы, которые приведут меня к поиску решения этой проблемы. Благодарю.
17 ответов
Вы можете использовать SKSpriteNode в качестве кнопки, а затем, когда пользователь касается, проверить, не был ли это касался узла. Используйте свойство name SKSpriteNode для идентификации узла:
//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;
}
Добавьте узел к вашей сцене:
[self addChild: [self fireButtonNode]];
Ручка касается:
//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...
}
}
Я сделал свой собственный класс кнопок, с которым я работаю. 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);
}
Пример: чтобы инициализировать кнопку, вы пишете следующие строки:
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];
Кроме того, вам нужен метод buttonAction в вашем классе. * Нет гарантии, что этот класс работает правильно в каждом случае. Я все еще довольно плохо знаком с объективом-c. *
Для людей, пишущих свои игры в Swift! Я переписал основные части решения Графа для быстрого класса. Надеюсь, поможет:
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)
}
}
}
Если вы хотите, вы можете использовать UIButton (или любой другой UIView).
Когда SKScene
создан, он еще не существует в SKView
, Вы должны реализовать didMoveToView:
на ваше SKScene
подкласс. На данный момент у вас есть доступ к SKView
сцена помещена, и вы можете добавить UIKit
возражает против этого. Для красивости я их поблек...
- (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) {
;
}];
}
вам нужно будет преднамеренно удалить их со сцены, когда вы уйдете, если, конечно, для них нет никакого смысла оставаться там.
- (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];
}];
}
}
Для тех, кто не знаком с программным созданием UIButton
Вот один пример (100 вещей можно сделать по-другому здесь) …
- (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;
}
Напоминание: UIView
происхождение в левом верхнем углу, SKScene
происхождение находится в левом нижнем углу.
Я использовал класс SKButton от Graf.
Я использую SKButton для навигации по сцене. т.е. представить другую сцену, когда пользователь нажимает кнопку SK. я получил EXC_BAD_ACCESS
ошибка в touchesEnded->[self setIsSelected:NO]
, Это происходит особенно часто на последних iPad с быстрым процессором.
После проверки и устранения неисправностей я понял, что объект SKButton уже "освобожден", когда setIsSelected
функция вызывается. Это потому, что я использую SKButton для перехода к следующей сцене, и это также означает, что текущая сцена может быть освобождена в любое время.
Я сделал небольшое изменение, поместив setIsSelected в часть "else" следующим образом.
Надеюсь, это поможет другим разработчикам, которые также видят ту же ошибку.
(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);
}
Вот еще одна версия, основанная на Swift-коде Филипа. Я просто немного упростил его и позволил принимать блоки, а не только селекторы:
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)
}
}
}
Используйте это так:
aFTButton.actionTouchUpInside = FTButtonTarget.aBlock({ () -> Void in
println("button touched")
})
Надеюсь это поможет.
Редактировать: я сделал github-репо для своего SKButtonNode, который, я надеюсь, будет постоянно обновляться по мере развития Swift!
К сожалению, я пока не могу комментировать быструю реализацию Филиппа SKButton в Swift. Супер рад, что он сделал это в Swift! Но я заметил, что он не включает функцию добавления текста к кнопке. Для меня это огромная функция, так что вам не нужно создавать отдельные ресурсы для каждой кнопки, а только фон и добавлять динамический текст.
Я добавил простую функцию для добавления текстовой метки в SKButton. Это, вероятно, не идеально - я новичок в Swift, как и все остальные! Не стесняйтесь комментировать и помогите мне обновить это как можно лучше. Надеюсь, вам, ребята, нравится!
//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
}
Пример создания кнопки:
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);
Полный класс приведен ниже:
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)
}
}
}
Как много хороших решений этой проблемы! Для хардкорных скроллеров, которые делают это так далеко, вас ждет удовольствие! Я подкласс SKScene
и требуется ОДИН вызов функции для регистрации ЛЮБОГО узла, чтобы он действовал как UIButton
! Вот класс:
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
}
}
//------------------------------------------------------------------------------------
}
Он включает в себя множество идей, которые я еще не реализовал, и некоторые пояснения к коду, но просто скопируйте и вставьте его в свой проект, и вы можете использовать его как есть на своей собственной сцене. Вот полный пример использования:
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)
}
}
Одно предостережение, если вы реализуете touchesBegan
, touchesMoved
, touchesEnded
и / или touchesCancelled
Вы ДОЛЖНЫ НАЗВАТЬ СУПЕР! Или иначе это не будет работать.
И, пожалуйста, поймите, что в этом примере, на самом деле есть только ОДНА ЛИНИЯ КОДА, которую нужно дать ЛЮБОМУ УЗЛУ UIButton
характеристики! Это была эта строка:
addButton(playButton, withCompletionHandler: playButtonPushed)
Я всегда открыт для идей и предложений. Оставьте их в комментариях и счастливого кодирования!
К сожалению, я забыл упомянуть, что я использую это отличное расширение. Вы можете взять его из расширения (так как оно, вероятно, вам не нужно в каждом узле) и добавить его в мой класс. Я использую это только в одном месте.
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
}
}
Вот простая кнопка, написанная на современном Swift (4.1.2)
Характеристики
- он принимает 2 имени изображения, 1 для состояния по умолчанию и одно для активного состояния
- разработчик может установить
touchBeganCallback
а такжеtouchEndedCallback
замыкания, чтобы добавить пользовательское поведение
Код
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?()
}
}
Как это использовать
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)
}
}
Мое решение для решения этой проблемы написано полностью в SWIFT, используя замыкания.
Его довольно просто использовать! 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..") }
}
На самом деле это хорошо работает на Swift 2.2 на Xcode 7.3
Мне нравится FTButtonNode ( richy486 / FTButtonNode.swift), но невозможно указать другой размер (а не размер текстуры по умолчанию) непосредственно во время инициализации, поэтому я добавил этот простой метод:
Вы должны скопировать его в соответствии с официальным пользовательским методом init (похожим на этот), чтобы у вас был другой метод init:
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)
}
Еще одна важная вещь - "время выбора", я видел, что в новых устройствах (iPhone 6) иногда между touchesBegan
а также touchesEnded
слишком быстро, и вы не видите изменений между defaultTexture
а также selectedTexture
,
С этой функцией:
func dispatchDelay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
Вы можете переписать touchesEnded
метод, чтобы правильно показать изменение текстуры:
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)
}
}
У решения Графа есть одна проблема. Например:
self.pauseButton = [[AGSKBButtonNode alloc] initWithImageNamed:@"ButtonPause"];
self.pauseButton.position = CGPointMake(0, 0);
[self.pauseButton setTouchUpInsideTarget:self action:@selector(pauseButtonPressed)];
[_hudLayer addChild:_pauseButton];
_hudLayer - это SKNode, собственность моей сцены. Таким образом, вы получите исключение из-за метода touchesEnded в SKButton. Он будет вызывать [SKSpriteNode pauseButtonPressed], а не со сценой.
Решение изменить self.parent, чтобы коснуться цели:
- (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];
}}
Я создал класс для использования SKSpriteNode в качестве кнопки довольно давно. Вы можете найти его на GitHub здесь.
Его реализация основана на UIButton, поэтому, если вы уже знакомы с iOS, вам будет легко с ним работать.
Ему также может быть назначен блок или SKAction, которые будут выполняться при нажатии кнопки.
Он также включает метод настройки метки.
Кнопка обычно объявляется так:
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];
И это все. Тебе хорошо идти.
Я подкласс SKScene
класс и достиг проблемы решения нажатий кнопок в этом проекте.
https://github.com/Prasad9/SpriteKitButton
В нем должны быть названы все узлы, которые необходимо знать при постукивании.
В дополнение к обнаружению нажатия кнопки, этот проект также позволяет определить, началось или закончилось касание определенного узла.
Чтобы получить действие касания, переопределите следующий метод в своем файле сцены.
- (void)touchUpInsideOnNodeName:(NSString *)nodeName atPoint:(CGPoint)touchPoint {
// Your code here.
}
Чтобы узнать начало прикосновения к определенному телу, переопределите следующий метод в файле Scene.
- (void)touchBeginOnNodeName:(NSString *)nodeName {
// Your code here.
}
Чтобы узнать конец касания определенного тела, переопределите следующий метод в файле сцены.
- (void)touchEndedOnNodeName:(NSString *)nodeName {
// Your code here.
}
К сожалению SpriteKit не имеет кнопки узла, я не знаю почему, потому что это очень полезный элемент управления. Поэтому я решил создать свой собственный и поделиться через CocoaPods, пожалуйста, используйте его OOButtonNode. Кнопки могут использовать текст / фон или изображения, написанные в Swift 4.
И поскольку все мы не ориентируемся на iOS, вот начало кода, который я написал для взаимодействия с мышью на Mac.
Вопрос к гуру: MacOS предлагает сенсорные события при использовании трекпада? Или они отправляются в SpriteKit как события мыши?
Другой вопрос для гуру, не должен ли этот класс должным образом называться SKButtonNode?
В любом случае, попробуйте это...
#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
Я не был убежден ни в одном из перечисленных выше вариантов, поэтому на основе последней версии Swift4 я создал собственное решение.