Hide and unhide buttons in a SKScene - swift

In a game I'm developing with Swift, I have a SKScene in which the player can view different backgrounds to choose from and select one. The backgrounds fill the back and there are buttons that allow one to see the next or previous backgrounds. I've already tested a 'select' button that saves the current background and makes a transition to the Game Scene. Now I want to show different 'select' buttons depending on the background, each button will show a different price and will subtract a different amount to the player's coins.
My code can currently change the back when the player taps the 'next' and 'previous' buttons. But I'm having trouble showing the 'select' buttons for each back. Here's the relevant parts of my code:
import SpriteKit
class ShopScene: SKScene {
var backNumber = 100
var backRemainder = 0
var background = SKSpriteNode()
var coinNumber = UserDefaults.standard.integer(forKey: "coinSaved")
var backName:String? = UserDefaults.standard.string(forKey: "backSaved")
override func didMove(to view: SKView) {
if backName != nil {
backName = UserDefaults.standard.string(forKey: "backSaved")
} else {
backName = "back1"
}
background.texture = SKTexture(imageNamed: "\(backName!)")
self.addChild(background)
let nextButton: NButton = NButton(defaultButtonImage: "next", activeButtonImage: "nextP", buttonAction: nextAction)
addChild(nextButton)
let previousButton: PButton = PButton(defaultButtonImage: "previous", activeButtonImage: "previousP", buttonAction: previousAction)
addChild(previousButton)
let selectButton: SButton = SButton(defaultButtonImage: "select", activeButtonImage: "selectP", buttonAction: selectAction)
addChild(selectButton)
func nextAction() {
backNumber += 1
backRemainder = backNumber % 2
switch backRemainder {
case 0:
backName = "back1"
case 1:
backName = "back2"
selectButton.isHidden = true
default:
backName = "back1"
}
UserDefaults.standard.set(backName, forKey: "backSaved")
background.texture = SKTexture(imageNamed: "\(backName!)")
}
func previousAction() {
backNumber -= 1
backRemainder = backNumber % 2
switch backRemainder {
case 0:
backName = "back1"
case 1:
backName = "back2"
selectButton.isHidden = true
default:
backName = "back1"
}
UserDefaults.standard.set(backName, forKey: "backSaved")
background.texture = SKTexture(imageNamed: "\(backName!)")
}
As you can see, I'm trying to use the isHidden property but I'm getting an error: "use of unresolved identifier 'selectButton'". I've tried initializing the button before didMove(toView) but it just messes things up as the selectAction() must be after the didMove(toView) block. I hope what I just wrote is not too confusing or wrong in some ways, I'm just learning to code with SpriteKit.
How can I hide and unhide buttons in a SKScene?

The error is because you are declaring your buttons in the didMove(to view: SKView) func. PreviousAction() won't know that those variables exist. You need to move their declarations up inside of the class not the func
class ShopScene: SKScene {
let nextButton: NButton!
let previousButton: PButton!
let selectButton: SButton!
override func didMove(to view: SKView) {
nextButton = NButton(defaultButtonImage: "next", activeButtonImage: "nextP", buttonAction: nextAction)
addChild(nextButton)
previousButton = PButton(defaultButtonImage: "previous", activeButtonImage: "previousP", buttonAction: previousAction)
addChild(previousButton)
selectButton = SButton(defaultButtonImage: "select", activeButtonImage: "selectP", buttonAction: selectAction)
addChild(selectButton)
}
}

Related

ARKit – How to know if 3d object is in the center of a screen?

I place 3d object in the world space. After that I try to move camera randomly. Then right now I need to know after I knew object has became inside frustum by isNode method, if the object is in center, top or bottom of camera view.
For a solution that's not a hack you can use the projectPoint: API.
It's probably better to work with pixel coordinates because this method uses the actual camera's settings to determine where the object appears on screen.
let projectedPoint = sceneView.projectPoint(self.sphereNode.worldPosition)
let xOffset = projectedPoint.x - screenCenter.x;
let yOffset = projectedPoint.y - screenCenter.y;
if xOffset * xOffset + yOffset * yOffset < R_squared {
// inside a disc of radius 'R' at the center of the screen
}
Solution
To achieve this you need to use a trick. Create new SCNCamera, make it a child of pointOfView default camera and set its FoV to approximately 10 degrees.
Then inside renderer(_:updateAtTime:) instance method use isNode(:insideFrustumOf:) method.
Here's working code:
import ARKit
class ViewController: UIViewController,
ARSCNViewDelegate,
SCNSceneRendererDelegate {
#IBOutlet var sceneView: ARSCNView!
#IBOutlet var label: UILabel!
let cameraNode = SCNNode()
let sphereNode = SCNNode()
let config = ARWorldTrackingConfiguration()
public func renderer(_ renderer: SCNSceneRenderer,
updateAtTime time: TimeInterval) {
DispatchQueue.main.async {
if self.sceneView.isNode(self.sphereNode,
insideFrustumOf: self.cameraNode) {
self.label.text = "In the center..."
} else {
self.label.text = "Out OF CENTER"
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.allowsCameraControl = true
let scene = SCNScene()
sceneView.scene = scene
cameraNode.camera = SCNCamera()
cameraNode.camera?.fieldOfView = 10
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.sceneView.pointOfView!.addChildNode(self.cameraNode)
}
sphereNode.geometry = SCNSphere(radius: 0.05)
sphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red
sphereNode.position.z = -1.0
sceneView.scene.rootNode.addChildNode(sphereNode)
sceneView.session.run(config)
}
}
Also, in this solution you may turn on an orthographic projection for child camera, instead of perspective one. It helps when a model is far from the camera.
cameraNode.camera?.usesOrthographicProjection = true
Here's how your screen might look like:
Next steps
The same way you can append two additional SCNCameras, place them above and below central SCNCamera, and test your object with two extra isNode(:insideFrustumOf:) instance methods.
I solved problem with another way:
let results = self.sceneView.hitTest(screenCenter!, options: [SCNHitTestOption.rootNode: parentnode])
where parentnode is the parent of target node, because I have multiple nodes.
func nodeInCenter() -> SCNNode? {
let x = (Int(sceneView.projectPoint(sceneView.pointOfView!.worldPosition).x - sceneView.projectPoint(sphereNode.worldPosition).x) ^^ 2) < 9
let y = (Int(sceneView.projectPoint(sceneView.pointOfView!.worldPosition).y - sceneView.projectPoint(sphereNode.worldPosition).y) ^^ 2) < 9
if x && y {
return node
}
return nil
}

ios - Swift 4: How to remove UIImageViews when UIImageView.animationImages is finished?

I am using .animationImages for UIImageViews in my project (it's a game). When the user presses a hint button I programmatically create a variable number of UIImageViews that will appear on screen and play the same image sequence once. I need to remove the UIImageViews from the view when the animation has stopped but I can't figure out how to do this.
I've been through the swift documentation for both .animationImages and .startAnimating but it's literally one paragraph and one line, neither of which is in relation to a completion or finish notifications etc. Any help will be much appreciated.
private func hideAnswerButtonsHint() {
// Clear all of the answer boxes that aren't hint locked
resetAnswerBoxes()
var imageArray = [UIImage]()
var remainingLetters = [String]()
for imageCount in 1...8 {
let imageName = "green_smoke_puff_0\(imageCount)"
let image = UIImage(named: imageName)!
imageArray.append(image)
}
for answerBox in answerBoxArray where answerBox.hintLock == false {
if let letter = answerBoxDict[answerBox.tag] {
remainingLetters.append(letter)
}
}
for answerButton in answerButtonArray where answerButton.hintLock == false {
if let index = remainingLetters.index(of: answerButton.titleText) {
answerButton.decompress()
remainingLetters.remove(at: index)
} else {
let frame = answerButton.superview?.convert(answerButton.frame.origin, to: nil)
let imageView = UIImageView()
imageView.animationImages = imageArray
imageView.animationDuration = 0.5
imageView.animationRepeatCount = 1
imageView.frame = CGRect(x: frame!.x, y: frame!.y, width: answerButton.frame.width, height: answerButton.frame.height)
view.addSubview(imageView)
imageView.startAnimating()
answerButton.compress()
answerButton.hintLock = true
}
}
}
UIImageView is an NSObject so you can always use KVO to watch its properties:
var observation: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
observation = imageView.observe(\.isAnimating) { [weak self] imageView, change in
if let value = change.newValue,
value == true {
imageView.removeFromSuperview()
self?.observation = nil
}
}
}

I want to limit the number of taps

Each tap adds an object on my scene, but I want to add it only once and then disable the Tap gesture. I looked everywhere but none of them are working with my code. Can someone help me with this? Either limiting the tap to 1 only or disabling it. I tried adding the tap gesture as Outlet and then setting .isEnabled = false but it is still not working.
class ARScene: UIViewController, ARSCNViewDelegate, UIGestureRecognizerDelegate {
#IBOutlet weak var sceneView: ARSCNView!
var tap : UITapGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
self.sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]
let configuration = ARWorldTrackingConfiguration()
self.sceneView.session.run(configuration)
// Set the view's delegate
sceneView.delegate = self
sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints]
// Show statistics such as fps and timing information
sceneView.showsStatistics = true
// Create a new scene
// let scene = SCNScene(named: "art.scnassets/ship.scn")!
// Set the scene to the view
//sceneView.scene = scene
registerGestureRecognizer()
}
func registerGestureRecognizer(){
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
tap.numberOfTapsRequired = 1
sceneView.addGestureRecognizer(tap)
}
#objc func handleTap(gestureRecognizer: UIGestureRecognizer){
//let touchLocation = gestureRecognizer.location(in: sceneView)
let sceneLocation = gestureRecognizer.view as! ARSCNView
let touchLocation = gestureRecognizer.location(in: sceneLocation)
let hitResult = self.sceneView.hitTest(touchLocation, types: [.existingPlaneUsingExtent, .estimatedHorizontalPlane])
if hitResult.count > 0 {
guard let hitTestResult = hitResult.first else{
return
}
let node = SCNNode()
let scene = SCNScene(named: "art.scnassets/bucket/Bucket2.scn")
let nodeArray = scene!.rootNode.childNodes
for childNode in nodeArray{
node.addChildNode(childNode as SCNNode)
}
let worldPos = hitTestResult.worldTransform
node.scale = SCNVector3Make(0.009,0.009,0.009);
node.position = SCNVector3(x: 0, y: worldPos.columns.3.y, z: -1.4)
sceneView.scene.rootNode.addChildNode(node)
tap.isEnabled = false //NOT WORKING, I want to stop tap gesture here
}
}
In order to disable your tapGesture you need to have reference to the same gesture instance you assign tap to.
There are two ways either create global instance so you can change its properties like Enable/Disable from anywhere
Or access the variable from the action/method for that gesture.
Create the global varibale for your tap
like
var tap: UITapGestureRecognizer! // global varibale
func registerGestureRecognizer(){
tap = UITapGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
tap.numberOfTapsRequired = 1
sceneView.addGestureRecognizer(tap)
}
and then disable this in taphandle
tap.isEnabled = false // disable the gesture
Or
2 Update your handle methods to
#objc func handleTap(withTapRecognizer tapGesture: UITapGestureRecognizer) {
........
tapGesture.isEnabled = false // use the instance variable and disable the gesture this will disable the gesture from which its been called
}
For you to access the gesture in the function, you need to initialize it using
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(gestureRecognizer:)))
Then in your handleTap method, you can this at the end of the function
gestureRecognizer.view?.removeGestureRecognizer(gestureRecognizer)

How to act on the label control of a mouse down in OS X

In my class ViewController: NSViewController
I have the following code:
#IBAction override func mouseDown(theEvent: NSEvent) {
self.myLabel.textColor = NSColor.redColor()
//either of these work to set the labels text value
self.myLabel.objectValue = "Hello World"
self.myLabel.stringValue = "This is a test"
switch(self) {
case self.myLabel:
//change text of myLabel
break;
case self.myLabel1:
//change text of myLabel1
break;
case self.myLabel2:
//change text of myLabel2
break;
}
}
This works to change the text and color of a Label Control called myLabel, but I have 3 Label controls on the View how do I change the color on the one that sends the mouse down event? The only way I can think to do it is with the switch statement as in the code above. But I think there is a better way by some how using the sender of the event?
I'm new to OS X and Mac development and come from the .NET C# world so thanks for helping this Mac noob! Using the latest Swift and Xcode.
I did figure out how to determine if one of my NSTextField controls was clicked on in the mouseDown event of the view:
#IBAction override func mouseDown(theEvent: NSEvent) {
var event_location: NSPoint!
event_location = theEvent.locationInWindow
self.mouseDownEvent = theEvent
var cntrl_id = NSTextField()
var cntrl_frame = NSRect()
var cntrl_name = String()
var cntrl_value = String()
var hit = Bool()
for view in self.view.subviews as [NSView] {
if let ct = view as? NSTextField {
cntrl_name = ct.identifier!
cntrl_id = ct
cntrl_frame = ct.frame
cntrl_value = ct.stringValue
hit = cntrl_frame.contains(event_location)
if hit {
controlToMove = cntrl_id
break
}
}
}
}
There are probably ways to make this more efficient possibly such as keeping a dictionary of all the NSTextField controls on the view, and then
check the dictionary for if I have a "hit" on one of the controls.

Accessibility (Voice Over) with Sprite Kit

I'm attempting to add support for Voice Over accessibility in a puzzle game which has a fixed board. However, I'm having trouble getting UIAccessibilityElements to show up.
Right now I'm overriding accessibilityElementAtIndex, accessibilityElementCount and indexOfAccessibilityElement in my SKScene.
They are returning an array of accessible elements as such:
func loadAccessibleElements()
{
self.isAccessibilityElement = false
let pieces = getAllPieces()
accessibleElements.removeAll(keepCapacity: false)
for piece in pieces
{
let element = UIAccessibilityElement(accessibilityContainer: self.usableView!)
element.accessibilityFrame = piece.getAccessibilityFrame()
element.accessibilityLabel = piece.getText()
element.accessibilityTraits = UIAccessibilityTraitButton
accessibleElements.append(element)
}
}
Where piece is a subclass of SKSpriteNode and getAccessibilityFrame is defined:
func getAccessibilityFrame() -> CGRect
{
return parentView!.convertRect(frame, toView: nil)
}
Right now one (wrongly sized) accessibility element seems to appear on the screen in the wrong place.
Could someone point me in the right direction?
Many thanks
EDIT:
I've tried a hack-ish work around by placing a UIView over the SKView with UIButton elements in the same location as the SKSpriteNodes. However, accessibility still doesn't want to work. The view is loaded as such:
func loadAccessibilityView()
{
view.isAccessibilityElement = false
view.accessibilityElementsHidden = false
skView.accessibilityElementsHidden = false
let accessibleSubview = UIView(frame: view.frame)
accessibleSubview.userInteractionEnabled = true
accessibleSubview.isAccessibilityElement = false
view.addSubview(accessibleSubview)
view.bringSubviewToFront(accessibleSubview)
let pieces = (skView.scene! as! GameScene).getAllPieces()
for piece in pieces
{
let pieceButton = UIButton(frame: piece.getAccessibilityFrame())
pieceButton.isAccessibilityElement = true
pieceButton.accessibilityElementsHidden = false
pieceButton.accessibilityTraits = UIAccessibilityTraitButton
pieceButton.setTitle(piece.getText(), forState: UIControlState.Normal)
pieceButton.setBackgroundImage(UIImage(named: "blue-button"), forState: UIControlState.Normal)
pieceButton.alpha = 0.2
pieceButton.accessibilityLabel = piece.getText()
pieceButton.accessibilityFrame = pieceButton.frame
pieceButton.addTarget(self, action: Selector("didTap:"), forControlEvents: UIControlEvents.TouchUpInside)
accessibleSubview.addSubview(pieceButton)
}
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil)
}
The buttons are placed correctly, however accessibility just isn't working at all. Something seems to be preventing it from working.
I've searched in vain for a description of how to implement VoiceOver in Swift using SpriteKit, so I finally figured out how to do it. Here's some working code that converts a SKNode to an accessible pushbutton when added to a SKScene class:
// Add the following code to a scene where you want to make the SKNode variable named “leave” an accessible button
// leave must already be initialized and added as a child of the scene, or a child of other SKNodes in the scene
// screenHeight must already be defined as the height of the device screen, in points
// Accessibility
private var accessibleElements: [UIAccessibilityElement] = []
private func nodeToDevicePointsFrame(node: SKNode) -> CGRect {
// first convert from frame in SKNode to frame in SKScene's coordinates
var sceneFrame = node.frame
sceneFrame.origin = node.scene!.convertPoint(node.frame.origin, fromNode: node.parent!)
// convert frame from SKScene coordinates to device points
// sprite kit scene origin is in lower left, accessibility device screen origin is at upper left
// assumes scene is initialized using SKSceneScaleMode.Fill using dimensions same as device points
var deviceFrame = sceneFrame
deviceFrame.origin.y = CGFloat(screenHeight-1) - (sceneFrame.origin.y + sceneFrame.size.height)
return deviceFrame
}
private func initAccessibility() {
if accessibleElements.count == 0 {
let accessibleLeave = UIAccessibilityElement(accessibilityContainer: self.view!)
accessibleLeave.accessibilityFrame = nodeToDevicePointsFrame(leave)
accessibleLeave.accessibilityTraits = UIAccessibilityTraitButton
accessibleLeave.accessibilityLabel = “leave” // the accessible name of the button
accessibleElements.append(accessibleLeave)
}
}
override func didMoveToView(view: SKView) {
self.isAccessibilityElement = false
leave.isAccessibilityElement = true
}
override func willMoveFromView(view: SKView) {
accessibleElements = []
}
override func accessibilityElementCount() -> Int {
initAccessibility()
return accessibleElements.count
}
override func accessibilityElementAtIndex(index: Int) -> AnyObject? {
initAccessibility()
if (index < accessibleElements.count) {
return accessibleElements[index] as AnyObject
} else {
return nil
}
}
override func indexOfAccessibilityElement(element: AnyObject) -> Int {
initAccessibility()
return accessibleElements.indexOf(element as! UIAccessibilityElement)!
}
Accessibility frames are defined in the fixed physical screen coordinates, not UIView coordinates, and transforming between them is kind of tricky.
The device origin is the lower left of the screen, with X up, when the device is in landscape right mode.
It's a pain converting, I've no idea why Apple did it that way.