I am wondering how I could detect key presses in Xcode - my goal is to be able to detect when keys are pressed, specifically the space, escape, enter and WASD keys.
What I have tried
I have tried pressesBegan, pressesEnded and its other 'variants', and I have also tried keyDown. After looking for 2 hours, the only ways I could find to detect key presses were from the two methods listed above, which don't seem to be working for me.
When using pressesBegan, I tried putting the following code in both the GameViewController.swift file and the AppDelegate.swift file, however it doesn't seem to work:
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard let key = presses.first?.key else { return }
print(key.keyCode)
}
The code shows no errors, and even when I put a print file at the very start of the function to see if it is being called it does not give any output.
After this, I tried a keyDown function (in the GameViewController.swift file), however it threw the error "Cannot find type NSEvent in scope". I fixed this error by adding ", NSObject" to the top of the file in the line "class GameViewController: UIViewController {" but that threw the error "Multiple inheritance from classes 'UIViewController' and 'NSObject'". I have looked for about an hour but there are no solutions which match my issue/have not worked for me.
import UIKit
import SpriteKit
import GameplayKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = SKScene(fileNamed: "SplashScreen") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override var prefersStatusBarHidden: Bool {
return true
}
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard let key = presses.first?.key else { return }
switch key.keyCode {
case .keyboardR:
print("Roll dice")
case .keyboardH:
print("Show help")
default:
super.pressesBegan(presses, with: event)
}
}
override func keyDown(with event: NSEvent) {
print("a")
}
}
If you need any additional things I'll try to get back to you as quick as possible. I'm really sorry if I've missed something extremely obvious like putting it in a different file.
Thank you!
the pressesEnd() method works in the same way; Try to import UIKit first:
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard let key = presses.first?.key else { return }
switch key.keyCode {
case .keyboardSpacebar:
print("Continue the quiz…")
default:
super.pressesEnded(presses, with: event)
}
}
More can be found at:
https://www.hackingwithswift.com/example-code/uikit/how-to-detect-keyboard-input-using-pressesbegan-and-pressesended
in Apple's ARKitExample, if i add multiple virtual objects(two chair) in one scene view. How to detect which one chair i touched in sceneView at ARKitExample?
the sceneView.hitTest() function will return the array of SCNHitTestResult, but the result.node is kind class of SCNNode, i don't know the object i touched is which one chair?
Dose anyone could help this? Thanks a lots
You are respondsible for tracking which nodes belong to which objects. I usually use a Set since SCNNode is hashable. You can then easily test if the node belongs to one of the objects you are interested in:
guard let result = sceneView.hitTest(location, options: nil).first else {
return
}
if myObjectNodes.contains(result.node) { //myObjectNodes is declared as Set<SCNNode>
//This is a match
}
To elaborate on the answer from #JoshHomann, you can do something like this
Swift 4
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first as! UITouch
if(touch.view == self.sceneView){
print("touch working")
let viewTouchLocation:CGPoint = touch.location(in: sceneView)
guard let result = sceneView.hitTest(viewTouchLocation, options: nil).first else {
return
}
if myObjectNodes.contains(result.node) { //myObjectNodes is declared as Set<SCNNode>
print("match")
}
}
}
I have developed a game in Xcode using sprite kit, and scenes. Now I am trying to integrate the functionality to post high scores to twitter and Facebook. I've looked around, and most people say to use SLComposeServiceViewController which is fine, until I try and present it. Because my app really only uses scenes, they never have the member function "presentViewController(....)". Thus, I am unable to ever present it. Anyone know any way around this?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch:UITouch = touches.first!
let touchLocation = touch.location(in: self)
let touchedNode = self.atPoint(touchLocation)
if (touchedNode.name == "tryAgain") {
let nextScene = Scene_LiveGame(size: self.scene!.size)
nextScene.scaleMode = self.scaleMode
self.view?.presentScene(nextScene, transition: SKTransition.fade(withDuration: 0.5))
}
else if (touchedNode.name == "share") {
if SLComposeViewController.isAvailable(forServiceType: SLServiceTypeFacebook) {
let fShare = SLComposeViewController(forServiceType: SLServiceTypeFacebook)
self.presentViewController(fShare!, animated: true, completion: nil)
//^This is where my problem is. Xcode is telling me that self has no member function presentViewController which I totally understand, because its a scene and thus doesn't share those functions. But every resource online has shown me this is the only way to do it
}
}
Your are getting this error because you need to present a UIViewController from another UIViewController. So
self.presentViewController(...)
will not work because self (SKScene) is not a UIViewController. To present from a SKScene you would have to say this
view?.window?.rootViewController?.presentViewController(fShare!, animated: true, completion: nil)
I would recommend that you do not use those APIs anymore. Its better to use a UIActivityViewController for your sharing needs. This way you only need one share button in your app and you can share to all sorts of services (email, Twitter, Facebook, iMessage, WhatsApp etc).
Create a new Swift file and add this code.
enum ShareMenu {
static func open(text: String, image: UIImage?, appStoreURL: String?, from viewController: UIViewController?) {
guard let viewController = viewController, let view = viewController.view else { return }
// Activity items
var activityItems = [Any]()
// Text
activityItems.append(text)
// Image
if let image = image {
activityItems.append(image)
}
/// App url
if let appStoreURL = appStoreURL {
let items = ActivityControllerItems(appStoreURL: appStoreURL)
activityItems.append(items)
}
// Activity controller
let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
// iPad settings
if UIDevice.current.userInterfaceIdiom == .pad {
activityController.modalPresentationStyle = .popover
activityController.popoverPresentationController?.sourceView = view
activityController.popoverPresentationController?.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
activityController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.init(rawValue: 0)
}
// Excluded activity types
activityController.excludedActivityTypes = [
.airDrop,
.print,
.assignToContact,
.addToReadingList,
]
// Present
DispatchQueue.main.async {
viewController.present(activityController, animated: true)
}
// Completion handler
activityController.completionWithItemsHandler = { (activity, success, items, error) in
guard success else {
if let error = error {
print(error.localizedDescription)
}
return
}
// do something if needed
}
}
}
// MARK: - Activity Controller Items
/**
ActivityControllerItems
*/
private final class ActivityControllerItems: NSObject {
// MARK: - Properties
/// App name
fileprivate let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "-"
/// App store web url
fileprivate let appStoreURL: String
// MARK: - Init
/// Init
fileprivate init(appStoreURL: String) {
self.appStoreURL = appStoreURL
super.init()
}
}
// MARK: - UIActivityItemSource
/// UIActivityItemSource
extension ActivityControllerItems: UIActivityItemSource {
/// Getting data items
/// Placeholder item
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return ""
}
/// Item for actity type
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivityType) -> Any? {
return URL(string: appStoreURL) ?? appName
}
/// Provide info about data items
/// Subject field for services such as email
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivityType?) -> String {
return appName
}
}
Than when the share button is pressed you can call it like so
ShareMenu.open(
text: "Can you beat my score?",
image: UIImage(...), // set to nil if unused
appStoreURL: "your iTunes app store URL", // set to nil if unused
from: view?.window?.rootViewController
)
Bear in mind that the image and appStoreURL will not show up everywhere, it depends on the sharing service.
You can also use your score value from your scene and add it to the text e.g
ShareMenu.open(
text: "Can you beat my score \(self.score)?",
...
)
Hope this helps
I will not go into SLComposeViewController related code. I will just show you two techniques aside from what crashoverride777 proposed. So the first technique would be using notifications, like this:
GameScene:
import SpriteKit
let kNotificationName = "myNotificationName"
class GameScene: SKScene {
private func postNotification(named name:String){
NotificationCenter.default.post(
Notification(name: Notification.Name(rawValue: name),
object: self,
userInfo: ["key":"value"]))
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.postNotification(named: kNotificationName)
}
}
Here, you post a notification by tapping the screen. A desired view controller class can listen for this notification, like this:
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handle(notification:)),
name: NSNotification.Name(rawValue: kNotificationName),
object: nil)
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = GameScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
view.presentScene(scene)
}
}
}
func handle(notification:Notification){
print("Notification : \(notification)")
}
}
Here, we add self as an observer for this notification - means that when notification happens, an appropriate handling method will be called (and that is our custom handle(notification:) method. In that method, you should call your code:
if SLComposeViewController.isAvailable(forServiceType: SLServiceTypeFacebook) {
let fShare = SLComposeViewController(forServiceType: SLServiceTypeFacebook)
self.presentViewController(fShare!, animated: true, completion: nil)
}
Actually, I will write another example for delegation, to keep things clean :)
As I said, this can be done using notifications, like in this answer, or you can go with delegation:
First you should declare the MyDelegate protocol which defines one method called myMethod().
protocol MyDelegate:class {
func myMethod()
}
That method is a requirement that every class must implement if it conforms to this protocl.
In our example, you can look at the scene as a worker, and a view controller as a boss. When the scene finishes its task, it notifies its boss (delegates responsibilities to him) about job completion, so that boss can decide what is next. I mean, I could say : "The scene is a boss, and it delegates responsibilities to his employee, the view controller..." But it doesn't really matter who you consider as a boss... The delegation pattern matters.
So, the view controller, should conform to this protocol, and it will implement the myMethod() (that will be called by the scene later):
class GameViewController: UIViewController, MyDelegate {
override func viewDidLoad() {
super.viewDidLoad()
//MARK: Conforming to MyDelegate protocol
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = GameScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
scene.myDelegate = self
// Present the scene
view.presentScene(scene)
}
}
}
func myMethod(){
print("Do your stuff here")
}
}
And here is a code from the GameScene where you define the myDelegate property that we use to communicate with our view controller:
import SpriteKit
class GameScene: SKScene {
weak var myDelegate:MyDelegate?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.myDelegate?.myMethod()
}
}
To find out when to choose delegation over notifications and vice-versa take a look at this article (or just search SO, there are some nice posts about that).
I want to be able to toggle my SKEmitterNode (rain particles) off and on based on the score. But my update function gets called constantly, i.e. I end up with millions of particles on the screen with my current code below...how can I structure my code so that the rain particles will only get called once when a score is achieved?
class GameScene: SKScene, SKPhysicsContactDelegate {
func setUpRain() {
if let rainParticle = SKEmitterNode(fileNamed: "Rain") {
rainParticle.position = CGPointMake(frame.size.width, frame.size.height)
rainParticle.name = "rainParticle"
rainParticle.zPosition = Layer.Flash.rawValue
worldNode.addChild(rainParticle)
}
}
func makeItRain() {
let startRaining = SKAction.runBlock {
self.setUpRain()
}
runAction(startRaining, withKey: "Rain")
}
func stopRaining() {
removeActionForKey("Rain")
worldNode.enumerateChildNodesWithName("rainParticle", usingBlock: { node, stop in
node.removeFromParent()
})
}
}
class PlayingState: GKState {
unowned let scene: GameScene //used to gain access to our scene
override func updateWithDeltaTime(seconds: NSTimeInterval) {
scene.updateForegroundAndBackground()
scene.updateScore()
if scene.score > 2 {
scene.makeItRain()
}
if scene.score > 4 {
scene.stopRaining()
}
}
There's a few ways you can do this, but the simplest of these is to only call makeItRain() or stopRaining() once per toggle. What I mean by this is once makeItRain is called, it cannot be called again until stopRaining is called. This can be done with a boolean like so:
var rainToggle: Bool = false; //True = Raining
override func updateWithDeltaTime(seconds: NSTimeInterval) {
scene.updateForegroundAndBackground()
scene.updateScore()
if (scene.score > 4){
scene.stopRaining()
rainToggle = false;
}
else if (scene.score > 2 && !rainToggle) {
scene.makeItRain()
rainToggle = true;
}
}
This is only slightly inefficient since you are calling stopRaining() every frame for no reason, however it gets the job done and is easy to understand. Note also that I had to flip the order in which your if statements came (otherwise it wouldn't work).
I have a simple player class like this:
enum State: Int {
case dead, alive, idle
}
class Player {
var playerSprite : SKSpriteNode
var currentState : State
init(passedInState: State) {
currentState = passedInState
self.playerSprite = SKSpriteNode(imageNamed: "playersprite.png")
switch currentState {
case .dead:
println("Player is dead")
case .alive:
println("Player is alive")
}
}
}
In my main class i create an instance of the Player class and try to change the players state by tapping on it in the touch method like so:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let player1 = Player(passedInState: .alive)
self.addChild(player1.playerSprite)
player1.playerSprite.position = CGPointMake(100, 100)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
let touch = touches.anyObject() as UITouch
let location = touch.locationInNode(self)
var locationSprites = nodesAtPoint(location)
for bla in locationSprites {
(bla as Player).currentState = .dead
}
}
}
I always get a crash when touching the Sprite. Do you have any idea why?
It seems there's a downcast failure, and in your code I see there are 2 forced donwcasts:
let touch = touches.anyObject() as UITouch
and
(bla as Player).currentState = .dead
I presume the error is in the last one, so I suggest setting a breakpoint and inspecting bla to check whether it's actually an instance of Player. Basing on your logic, if it can happen that it is not an instance of Player, I recommend using optional downcast:
if let player = bla as? Player {
player.currentState = .dead
}
The advantage is that if bla is not an instance of Player, the if block won't be executed, instead of making the app crash.
Comment follow up
If you want to do some custom processing when the player status changes, you can add a property observer to the currentState property of the Player class:
var currentState : State {
didSet {
switch currentState {
case .dead:
println("Player is dead")
case .alive:
println("Player is alive")
case .idle:
break
}
}
}