Finishes implementation of Sustain Buttons feature

This commit is contained in:
Riley Testut 2016-06-02 19:42:51 -05:00
parent 0f43de2138
commit c647762975
5 changed files with 200 additions and 55 deletions

@ -1 +1 @@
Subproject commit 1476246c71526677acf299907ec5ec4fbc3f6d89 Subproject commit 769ba80cc892f7267739a13719cb38bec3daf7e7

@ -1 +1 @@
Subproject commit 99aeaa0ce70be0d943c49a124f8781d08cacf634 Subproject commit 4a41f84576d0162f544a6d78268867b4d529262c

View File

@ -106,7 +106,7 @@
<rect key="frame" x="0.0" y="0.0" width="600" height="300"/> <rect key="frame" x="0.0" y="0.0" width="600" height="300"/>
<color key="backgroundColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="calibratedRGB"/> <color key="backgroundColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="calibratedRGB"/>
</view> </view>
<view alpha="0.0" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="noi-yo-HIE"> <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="noi-yo-HIE">
<rect key="frame" x="0.0" y="0.0" width="600" height="300"/> <rect key="frame" x="0.0" y="0.0" width="600" height="300"/>
<subviews> <subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pwD-5i-uQ2"> <visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pwD-5i-uQ2">

View File

@ -11,6 +11,26 @@ import UIKit
import DeltaCore import DeltaCore
import Roxas import Roxas
// Temporary wrapper around dispatch_semaphore_t until Swift 3 + modernized libdispatch
private struct DispatchSemaphore: Hashable
{
let semaphore: dispatch_semaphore_t
var hashValue: Int {
return semaphore.hash
}
init(value: Int)
{
self.semaphore = dispatch_semaphore_create(value)
}
}
private func ==(lhs: DispatchSemaphore, rhs: DispatchSemaphore) -> Bool
{
return lhs.semaphore.isEqual(rhs.semaphore)
}
class EmulationViewController: UIViewController class EmulationViewController: UIViewController
{ {
//MARK: - Properties - //MARK: - Properties -
@ -29,6 +49,13 @@ class EmulationViewController: UIViewController
private(set) var emulatorCore: EmulatorCore! { private(set) var emulatorCore: EmulatorCore! {
didSet didSet
{ {
// Cannot set directly, or else we're left with a strong reference cycle
//self.emulatorCore.updateHandler = emulatorCoreDidUpdate
self.emulatorCore.updateHandler = { [weak self] core in
self?.emulatorCoreDidUpdate(core)
}
self.preferredContentSize = self.emulatorCore.preferredRenderingSize self.preferredContentSize = self.emulatorCore.preferredRenderingSize
} }
} }
@ -42,6 +69,17 @@ class EmulationViewController: UIViewController
var deferredPreparationHandler: (Void -> Void)? var deferredPreparationHandler: (Void -> Void)?
//MARK: - Private Properties //MARK: - Private Properties
private var pauseViewController: PauseViewController?
private var pausingGameController: GameControllerProtocol?
private var context = CIContext(options: [kCIContextWorkingColorSpace: NSNull()])
private var updateSemaphores = Set<DispatchSemaphore>()
private var sustainedInputs = [ObjectIdentifier: [InputType]]()
private var reactivateSustainInputsQueue: NSOperationQueue
private var choosingSustainedButtons = false
@IBOutlet private var controllerView: ControllerView! @IBOutlet private var controllerView: ControllerView!
@IBOutlet private var gameView: GameView! @IBOutlet private var gameView: GameView!
@IBOutlet private var sustainButtonContentView: UIView! @IBOutlet private var sustainButtonContentView: UIView!
@ -49,22 +87,14 @@ class EmulationViewController: UIViewController
@IBOutlet private var controllerViewHeightConstraint: NSLayoutConstraint! @IBOutlet private var controllerViewHeightConstraint: NSLayoutConstraint!
private var pauseViewController: PauseViewController?
private var context = CIContext(options: [kCIContextWorkingColorSpace: NSNull()])
private var selectingSustainedButton = false {
didSet {
self.sustainButtonContentView.alpha = self.selectingSustainedButton ? 1.0 : 0.0
}
}
//MARK: - Initializers - //MARK: - Initializers -
/** Initializers **/ /** Initializers **/
required init?(coder aDecoder: NSCoder) required init?(coder aDecoder: NSCoder)
{ {
self.reactivateSustainInputsQueue = NSOperationQueue()
self.reactivateSustainInputsQueue.maxConcurrentOperationCount = 1
super.init(coder: aDecoder) super.init(coder: aDecoder)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(EmulationViewController.updateControllers), name: ExternalControllerDidConnectNotification, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(EmulationViewController.updateControllers), name: ExternalControllerDidConnectNotification, object: nil)
@ -96,8 +126,8 @@ class EmulationViewController: UIViewController
self.gameView.backgroundColor = UIColor.clearColor() self.gameView.backgroundColor = UIColor.clearColor()
self.emulatorCore.addGameView(self.gameView) self.emulatorCore.addGameView(self.gameView)
self.backgroundView.textLabel.text = NSLocalizedString("Select Button to Sustain", comment: "") self.backgroundView.textLabel.text = NSLocalizedString("Select Buttons to Sustain", comment: "")
self.backgroundView.detailTextLabel.text = NSLocalizedString("Sustained buttons act as though they're being held down, without needing to do so yourself. This is particularly useful for certain games, such as platformers which require you to hold down a button to run.", comment: "") self.backgroundView.detailTextLabel.text = NSLocalizedString("Press the Menu button when finished.", comment: "")
let controllerSkin = ControllerSkin.defaultControllerSkinForGameUTI(self.game.typeIdentifier) let controllerSkin = ControllerSkin.defaultControllerSkinForGameUTI(self.game.typeIdentifier)
@ -164,7 +194,7 @@ class EmulationViewController: UIViewController
coordinator.animateAlongsideTransition({ _ in coordinator.animateAlongsideTransition({ _ in
if self.pauseViewController != nil if self.emulatorCore.state == .Paused
{ {
// We need to manually "refresh" the game screen, otherwise the system tries to cache the rendered image, but skews it incorrectly when rotating b/c of UIVisualEffectView // We need to manually "refresh" the game screen, otherwise the system tries to cache the rendered image, but skews it incorrectly when rotating b/c of UIVisualEffectView
self.gameView.inputImage = self.gameView.outputImage self.gameView.inputImage = self.gameView.outputImage
@ -184,6 +214,10 @@ class EmulationViewController: UIViewController
if segue.identifier == "pauseSegue" if segue.identifier == "pauseSegue"
{ {
guard let gameController = sender as? GameControllerProtocol else { fatalError("sender for pauseSegue must be the game controller that pressed the Menu button") }
self.pausingGameController = gameController
let pauseViewController = segue.destinationViewController as! PauseViewController let pauseViewController = segue.destinationViewController as! PauseViewController
pauseViewController.pauseText = self.game.name pauseViewController.pauseText = self.game.name
@ -203,22 +237,24 @@ class EmulationViewController: UIViewController
pauseViewController.presentCheatsViewController(delegate: self) pauseViewController.presentCheatsViewController(delegate: self)
}) })
let sustainButtonItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Sustain Button", comment: ""), action: { [unowned self] item in var sustainButtonsItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Sustain Buttons", comment: ""), action: { [unowned self] item in
self.resetSustainedInputs(forGameController: gameController)
if item.selected if item.selected
{ {
self.selectingSustainedButton = true self.showSustainButtonView()
pauseViewController.dismiss() pauseViewController.dismiss()
} }
}) })
sustainButtonsItem.selected = self.sustainedInputs[ObjectIdentifier(gameController)]?.count > 0
var fastForwardItem = PauseItem(image: UIImage(named: "FastForward")!, text: NSLocalizedString("Fast Forward", comment: ""), action: { [unowned self] item in var fastForwardItem = PauseItem(image: UIImage(named: "FastForward")!, text: NSLocalizedString("Fast Forward", comment: ""), action: { [unowned self] item in
self.emulatorCore.fastForwarding = item.selected self.emulatorCore.fastForwarding = item.selected
}) })
fastForwardItem.selected = self.emulatorCore.fastForwarding fastForwardItem.selected = self.emulatorCore.fastForwarding
pauseViewController.items = [saveStateItem, loadStateItem, cheatCodesItem, fastForwardItem, sustainButtonItem] pauseViewController.items = [saveStateItem, loadStateItem, cheatCodesItem, fastForwardItem, sustainButtonsItem]
self.pauseViewController = pauseViewController self.pauseViewController = pauseViewController
} }
@ -227,6 +263,7 @@ class EmulationViewController: UIViewController
@IBAction func unwindFromPauseViewController(segue: UIStoryboardSegue) @IBAction func unwindFromPauseViewController(segue: UIStoryboardSegue)
{ {
self.pauseViewController = nil self.pauseViewController = nil
self.pausingGameController = nil
if self.resumeEmulation() if self.resumeEmulation()
{ {
@ -265,6 +302,11 @@ class EmulationViewController: UIViewController
/// Emulation /// Emulation
private extension EmulationViewController private extension EmulationViewController
{ {
func pause(sender sender: AnyObject?)
{
self.performSegueWithIdentifier("pauseSegue", sender: sender)
}
func pauseEmulation() -> Bool func pauseEmulation() -> Bool
{ {
return self.emulatorCore.pauseEmulation() return self.emulatorCore.pauseEmulation()
@ -272,14 +314,22 @@ private extension EmulationViewController
func resumeEmulation() -> Bool func resumeEmulation() -> Bool
{ {
guard !self.selectingSustainedButton && self.pauseViewController == nil else { return false } guard !self.choosingSustainedButtons && self.pauseViewController == nil else { return false }
return self.emulatorCore.resumeEmulation() return self.emulatorCore.resumeEmulation()
} }
func emulatorCoreDidUpdate(emulatorCore: EmulatorCore)
{
for semaphore in self.updateSemaphores
{
dispatch_semaphore_signal(semaphore.semaphore)
}
}
} }
//MARK: - Controllers/Inputs - //MARK: - Controllers -
/// Controllers/Inputs /// Controllers
private extension EmulationViewController private extension EmulationViewController
{ {
@objc func updateControllers() @objc func updateControllers()
@ -312,37 +362,118 @@ private extension EmulationViewController
self.view.setNeedsLayout() self.view.setNeedsLayout()
} }
}
func setSelectingSustainedButton(selectingSustainedButton: Bool, animated: Bool) //MARK: - Sustain Button -
private extension EmulationViewController
{ {
if !animated func showSustainButtonView()
{ {
self.selectingSustainedButton = selectingSustainedButton self.choosingSustainedButtons = true
self.sustainButtonContentView.hidden = false
} }
else
func hideSustainButtonView()
{ {
UIView.animateWithDuration(0.4) { self.choosingSustainedButtons = false
self.selectingSustainedButton = selectingSustainedButton
} UIView.animateWithDuration(0.4, animations: {
self.sustainButtonContentView.alpha = 0.0
}) { (finished) in
self.sustainButtonContentView.hidden = true
self.sustainButtonContentView.alpha = 1.0
} }
} }
func sustainInput(input: InputType, gameController: GameControllerProtocol) func resetSustainedInputs(forGameController gameController: GameControllerProtocol)
{ {
if let input = input as? ControllerInput if let previousInputs = self.sustainedInputs[ObjectIdentifier(gameController)]
{ {
guard input != ControllerInput.Menu else let receivers = gameController.receivers
{ receivers.forEach { gameController.removeReceiver($0) }
self.setSelectingSustainedButton(false, animated: true)
self.performSegueWithIdentifier("pauseSegue", sender: gameController) // Activate previousInputs without notifying anyone so we can then deactivate them
return // We do this because deactivating an already deactivated input has no effect
} previousInputs.forEach { gameController.activate($0) }
receivers.forEach { gameController.addReceiver($0) }
// Deactivate previously sustained inputs
previousInputs.forEach { gameController.deactivate($0) }
} }
self.setSelectingSustainedButton(false, animated: true) self.sustainedInputs[ObjectIdentifier(gameController)] = []
}
self.resumeEmulation() func addSustainedInput(input: InputType, gameController: GameControllerProtocol)
{
var inputs = self.sustainedInputs[ObjectIdentifier(gameController)] ?? []
guard !inputs.contains({ $0.isEqual(input) }) else { return }
inputs.append(input)
self.sustainedInputs[ObjectIdentifier(gameController)] = inputs
let receivers = gameController.receivers
receivers.forEach { gameController.removeReceiver($0) }
// Causes input to be considered deactivated, so gameController won't send a subsequent message to observers when user actually deactivates
// However, at this point the core still thinks it is activated, and is temporarily not a receiver, thus sustaining it
gameController.deactivate(input)
receivers.forEach { gameController.addReceiver($0) }
}
func reactivateSustainedInput(input: InputType, gameController: GameControllerProtocol)
{
// These MUST be performed serially, or else Bad Things Happen if multiple inputs are reactivated at once
self.reactivateSustainInputsQueue.addOperationWithBlock {
// The manual activations/deactivations here are hidden implementation details, so we won't notify ourselves about them
gameController.removeReceiver(self)
// Must deactivate first so core recognizes a secondary activation
gameController.deactivate(input)
let dispatchQueue = dispatch_queue_create("com.rileytestut.Delta.sustainButtonsQueue", DISPATCH_QUEUE_SERIAL)
dispatch_async(dispatchQueue) {
let semaphore = DispatchSemaphore(value: 0)
self.updateSemaphores.insert(semaphore)
// To ensure the emulator core recognizes us activating the input again, we need to wait at least two frames
// Unfortunately we cannot init DispatchSemaphore with value less than 0
// To compensate, we simply wait twice; once the first wait returns, we wait again
dispatch_semaphore_wait(semaphore.semaphore, DISPATCH_TIME_FOREVER)
dispatch_semaphore_wait(semaphore.semaphore, DISPATCH_TIME_FOREVER)
// These MUST be performed serially, or else Bad Things Happen if multiple inputs are reactivated at once
self.reactivateSustainInputsQueue.addOperationWithBlock {
self.updateSemaphores.remove(semaphore)
// Ensure we still are not a receiver (to prevent rare race conditions)
gameController.removeReceiver(self)
gameController.activate(input)
let receivers = gameController.receivers
receivers.forEach { gameController.removeReceiver($0) }
// Causes input to be considered deactivated, so gameController won't send a subsequent message to observers when user actually deactivates
// However, at this point the core still thinks it is activated, and is temporarily not a receiver, thus sustaining it
gameController.deactivate(input)
receivers.forEach { gameController.addReceiver($0) }
}
// More Bad Things Happen if we add self as observer before ALL reactivations have occurred (notable, infinite loops)
self.reactivateSustainInputsQueue.waitUntilAllOperationsAreFinished()
gameController.addReceiver(self)
}
}
} }
} }
@ -503,19 +634,33 @@ extension EmulationViewController: GameControllerReceiverProtocol
UIDevice.currentDevice().vibrate() UIDevice.currentDevice().vibrate()
} }
guard !self.selectingSustainedButton else if let input = input as? ControllerInput
{ {
self.sustainInput(input, gameController: gameController) switch input
{
case ControllerInput.Menu:
if self.choosingSustainedButtons { self.hideSustainButtonView() }
self.pause(sender: gameController)
// Return now, because Menu cannot be sustained
return
}
}
if self.choosingSustainedButtons
{
self.addSustainedInput(input, gameController: gameController)
return return
} }
guard let input = input as? ControllerInput else { return } if let sustainedInputs = self.sustainedInputs[ObjectIdentifier(gameController)] where sustainedInputs.contains({ $0.isEqual(input) })
print("Activated \(input)")
switch input
{ {
case ControllerInput.Menu: self.performSegueWithIdentifier("pauseSegue", sender: gameController) // Perform on next run loop
dispatch_async(dispatch_get_main_queue()) {
self.reactivateSustainedInput(input, gameController: gameController)
}
return
} }
} }

View File

@ -41,7 +41,7 @@ extension PauseMenuViewController
super.viewDidLoad() super.viewDidLoad()
let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout
collectionViewLayout.itemWidth = 90 collectionViewLayout.itemWidth = 95
collectionViewLayout.usesEqualHorizontalSpacingDistributionForSingleRow = true collectionViewLayout.usesEqualHorizontalSpacingDistributionForSingleRow = true
// Manually update prototype cell properties // Manually update prototype cell properties