diff --git a/Quake2-iOS.xcodeproj/project.pbxproj b/Quake2-iOS.xcodeproj/project.pbxproj index a0a0e4c..8d08e18 100644 --- a/Quake2-iOS.xcodeproj/project.pbxproj +++ b/Quake2-iOS.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 4C963DDC26BB446500F43256 /* JoyStickViewMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C963DDB26BB446400F43256 /* JoyStickViewMonitor.swift */; }; + 4C963DDD26BB447000F43256 /* JoyStickViewMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C963DDB26BB446400F43256 /* JoyStickViewMonitor.swift */; }; + 4C963DDE26BB447100F43256 /* JoyStickViewMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C963DDB26BB446400F43256 /* JoyStickViewMonitor.swift */; }; + 4C963DDF26BB447200F43256 /* JoyStickViewMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C963DDB26BB446400F43256 /* JoyStickViewMonitor.swift */; }; A132345A2209FFE200884138 /* Quake2mp1LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A13234582209FFD000884138 /* Quake2mp1LaunchScreen.storyboard */; }; A132345B2209FFE500884138 /* Quake2mp2LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A13234592209FFD000884138 /* Quake2mp2LaunchScreen.storyboard */; }; A142F96922138D03006A2A17 /* SavedGameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A142F96822138D02006A2A17 /* SavedGameViewController.swift */; }; @@ -1088,6 +1092,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 4C963DDB26BB446400F43256 /* JoyStickViewMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoyStickViewMonitor.swift; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Quake2-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Quake2-Info.plist"; sourceTree = ""; }; A13234582209FFD000884138 /* Quake2mp1LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Quake2mp1LaunchScreen.storyboard; sourceTree = ""; }; A13234592209FFD000884138 /* Quake2mp2LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Quake2mp2LaunchScreen.storyboard; sourceTree = ""; }; @@ -2351,6 +2356,7 @@ A1C1267C220908DF00EAD9CB /* Extensions.swift */, A15BF75F21FD0819005F4B74 /* GameViewController.swift */, A1C1205621FE821900EAD9CB /* JoyStickView.swift */, + 4C963DDB26BB446400F43256 /* JoyStickViewMonitor.swift */, A15BF74A21FCF7E3005F4B74 /* Main.storyboard */, A15BF75D21FD0604005F4B74 /* MainMenuViewController.swift */, A16571B8220B9FD900FE9FC9 /* OptionsViewController.swift */, @@ -3518,6 +3524,7 @@ A15BF73521FCDD7A005F4B74 /* sdl.c in Sources */, A15BF64521FCDD7A005F4B74 /* supertank.c in Sources */, A15BF67D21FCDD7A005F4B74 /* cmdparser.c in Sources */, + 4C963DDC26BB446500F43256 /* JoyStickViewMonitor.swift in Sources */, A1C1267D220908DF00EAD9CB /* Extensions.swift in Sources */, A15BF65121FCDD7A005F4B74 /* flipper.c in Sources */, A15BF70721FCDD7A005F4B74 /* gl3_surf.c in Sources */, @@ -3755,6 +3762,7 @@ A1C1237E2203B37600EAD9CB /* cl_network.c in Sources */, A1C123802203B37600EAD9CB /* videomenu.c in Sources */, A1C124052203B4B400EAD9CB /* medic.c in Sources */, + 4C963DDE26BB447100F43256 /* JoyStickViewMonitor.swift in Sources */, A1C123832203B37600EAD9CB /* CoreGraphics+Additions.swift in Sources */, A1C123E92203B49200EAD9CB /* g_monster.c in Sources */, A1C123F02203B49200EAD9CB /* g_utils.c in Sources */, @@ -3946,6 +3954,7 @@ A1C1247F2203FE2600EAD9CB /* unzip.c in Sources */, A1C124F72203FF4900EAD9CB /* client.c in Sources */, A1C124802203FE2600EAD9CB /* frame.c in Sources */, + 4C963DDF26BB447200F43256 /* JoyStickViewMonitor.swift in Sources */, A1C124812203FE2600EAD9CB /* misc.c in Sources */, A1C124DF2203FF2D00EAD9CB /* boss31.c in Sources */, A1C124822203FE2600EAD9CB /* argproc.c in Sources */, @@ -4336,6 +4345,7 @@ A1F1116A23F740E10030D586 /* sw_main.c in Sources */, A1F110C823F740B00030D586 /* qmenu.c in Sources */, A1F110C923F740B00030D586 /* cvar.c in Sources */, + 4C963DDD26BB447000F43256 /* JoyStickViewMonitor.swift in Sources */, A1F110CA23F740B00030D586 /* netchan.c in Sources */, A1F1116423F740E10030D586 /* sw_aclip.c in Sources */, A1F110CB23F740B00030D586 /* system.c in Sources */, diff --git a/Quake2-iOS/JoyStickView.swift b/Quake2-iOS/JoyStickView.swift index c9707ac..7817310 100644 --- a/Quake2-iOS/JoyStickView.swift +++ b/Quake2-iOS/JoyStickView.swift @@ -1,59 +1,86 @@ +// Copyright © 2020 Brad Howes. All rights reserved. + import UIKit import CoreGraphics - -protocol JoystickDelegate: class { - - func handleJoyStick(angle: CGFloat, displacement: CGFloat) - func handleJoyStickPosition(x: CGFloat, y: CGFloat) - -} - - -/** - Type definition for a function that will receive updates from the JoyStickView when the handle moves. Takes two - values, both CGFloats. - - parameter angle: the direction the handle is pointing. Unit is degrees with 0° pointing up (north), and 90° pointing - right (east). - - parameter displacement: how far from the view center the joystick is moved in the above direction. Unitless but - is the ratio of distance moved from center over the radius of the joystick base. Always in range 0.0-1.0 - */ -public typealias JoyStickViewMonitor = (_ angle: CGFloat, _ displacement: CGFloat) -> () - /** A simple implementation of a joystick interface like those found on classic arcade games. This implementation detects and reports two values when the joystick moves: - + * angle: the direction the handle is pointing. Unit is degrees with 0° pointing up (north), and 90° pointing right (east). * displacement: how far from the view center the joystick is moved in the above direction. Unitless but is the ratio of distance moved from center over the radius of the joystick base. Always in range 0.0-1.0 - - The view has two settable parameters: - - * monitor: a function of type `JoyStickViewMonitor` that will receive updates when the joystick's angle and/or - displacement values change. - * movable: a boolean that when true lets the joystick move around in its parent's view when there joystick moves + + The view has several settable parameters that be used to configure a joystick's appearance and behavior: + + - monitor: an enumeration of type `JoyStickViewMonitorKind` that can hold a function to receive updates when the + joystick's angle and/or displacement values change. Supports polar and cartesian (XY) reporting + - movable: a boolean that when true lets the joystick move around in its parent's view when there joystick moves beyond displacement of 1.0. + - movableBounds: a CGRect which limits where a movable joystick may travel + - baseImage: a UIImage to use for the joystick's base + - handleImage: a UIImage to use for the joystick's handle + Additional documentation is available via the attribute names below. */ -public final class JoyStickView: UIView { - - var delegate: JoystickDelegate? - - // override class var requiresConstraintBasedLayout: Bool { return true } - - /// Holds a function to call when joystick orientation changes - public var monitor: JoyStickViewMonitor? = nil - +@IBDesignable public final class JoyStickView: UIView { + + /// Optional monitor which will receive updates as the joystick position changes. Supports polar and cartesian + /// reporting. The function to call with a position report is held in the enumeration value. + public var monitor: JoyStickViewMonitorKind = .none + + /// Optional block to be called upon a tap + public var tappedBlock: (() -> Void)? + + /// Optional rectangular region that restricts where the handle may move. The region should be defined in + /// this view's coordinates. For instance, to constrain the handle in the Y direction with a UIView of size 100x100, + /// use `CGRect(x: 50, y: 0, width: 1, height: 100)` + public var handleConstraint: CGRect? { + didSet { + switch handleConstraint { + case .some(let hc): + handleCenterClamper = { CGPoint(x: min(max($0.x, hc.minX), hc.maxX), + y: min(max($0.y, hc.minY), hc.maxY)) } + default: + handleCenterClamper = { $0 } + } + } + } + + /// The last-reported angle from the joystick handle. Unit is degrees, with 0° up (north) and 90° right (east). + /// Note that this assumes that `angleRadians` was calculated with atan2(dx, dy) and that dy is positive when + /// pointing down. + public var angle: CGFloat { return displacement != 0.0 ? 180.0 - angleRadians * 180.0 / .pi : 0.0 } + + /// The last-reported displacement from the joystick handle. Dimensionless but is the ratio of movement over + /// the radius of the joystick base. Always falls between 0.0 and 1.0 + public private(set) var displacement: CGFloat = 0.0 + /// If `true` the joystick will move around in the parant's view so that the joystick handle is always at a /// displacement of 1.0. This is the default mode of operation. Setting to `false` will keep the view fixed. - public var movable: Bool = true - public var movableBounds: CGRect? - - /// The opacity of the base of the joystick. Note that this is different than the view's overall opacity setting. - /// The end result will be a base image with an opacity of `baseAlpha` * `view.alpha` - public var baseAlpha: CGFloat { + @IBInspectable public var movable: Bool = false + + /// The original location of a movable joystick. Used to restore its position when user double-taps on it. + public var movableCenter: CGPoint? = nil + + /// Optional rectangular region that restricts where the base may move. The region should be defined in the + /// this view's coordinates. + public var movableBounds: CGRect? { + didSet { + switch movableBounds { + case .some(let mb): + baseCenterClamper = { CGPoint(x: min(max($0.x, mb.minX), mb.maxX), + y: min(max($0.y, mb.minY), mb.maxY)) } + default: + baseCenterClamper = { $0 } + } + } + } + + /// The opacity of the base of the joystick. Note that this is different than the view's overall opacity + /// setting. The end result will be a base image with an opacity of `baseAlpha` * `view.alpha` + @IBInspectable public var baseAlpha: CGFloat { get { return baseImageView.alpha } @@ -61,43 +88,94 @@ public final class JoyStickView: UIView { baseImageView.alpha = newValue } } - - /// The tintColor to apply to the handle. By default, uses the view's tintColor value. Changing it while joystick - /// is visible will update the handle image. - public var handleTintColor: UIColor! { - didSet { makeHandleImage() } + + /// The opacity of the handle of the joystick. Note that this is different than the view's overall opacity setting. + /// The end result will be a handle image with an opacity of `handleAlpha` * `view.alpha` + @IBInspectable public var handleAlpha: CGFloat { + get { + return handleImageView.alpha + } + set { + handleImageView.alpha = newValue + } } - - /// The last-reported angle from the joystick handle. Unit is degrees, with 0° up (north) and 90° right (east) - public private(set) var angle: CGFloat = 0.0 - - /// The last-reported displacement from the joystick handle. Dimensionless but is the ratio of movement over - /// the radius of the joystick base. Always falls between 0.0 and 1.0 - public private(set) var displacement: CGFloat = 0.0 - - /// The radius of the base of the joystick, the max distance the handle may move in any direction. - private lazy var radius: CGFloat = { return self.bounds.size.width / 2.0 }() - + + /// The tintColor to apply to the handle. Changing it while joystick is visible will update the handle image. + @IBInspectable public var handleTintColor: UIColor? = nil { + didSet { generateHandleImage() } + } + + /// Scaling factor to apply to the joystick handle. A value of 1.0 will result in no scaling of the image, + /// however the default value is 0.85 due to historical reasons. + @IBInspectable public var handleSizeRatio: CGFloat = 0.85 { + didSet { + scaleHandleImageView() + } + } + + /// Control how the handle image is generated. When this is `false` (default), a CIFilter will be used to tint + /// the handle image with the `handleTintColor`. This results in a monochrome image of just one color, but with + /// lighter and darker areas depending on the original image. When this is `true`, the handle image is just + /// used as a mask, and all pixels with an alpha = 1.0 will be colored with the `handleTintColor` value. + @IBInspectable public var colorFillHandleImage: Bool = false { + didSet { generateHandleImage() } + } + + /// Controls how far the handle can travel along the radius of the base. A value of 1.0 (default) will let the + /// handle travel the full radius, with maximum travel leaving the center of the handle lying on the circumference + /// of the base. A value greater than 1.0 will let the handle travel beyond the circumference of the base, while a + /// value less than 1.0 will reduce the travel to values within the circumference. Note that regardless of this + /// value, handle movements will always report displacement values between 0.0 and 1.0 inclusive. + @IBInspectable public var travel: CGFloat = 1.0 + /// The image to use for the base of the joystick - private let baseImage = UIImage(named: "JoyStickBase")! - + @IBInspectable public var baseImage: UIImage? { + didSet { baseImageView.image = baseImage } + } + /// The image to use for the joystick handle - private let handleImage = UIImage(named: "JoyStickHandle")! + @IBInspectable public var handleImage: UIImage? { + didSet { generateHandleImage() } + } + + /// Control whether view will recognize a double-tap gesture and move the joystick base to its original location + /// when it happens. Note that this is only useful if `moveable` is true. + @IBInspectable public var enableDoubleTapForFrameReset = true { + didSet { + if let dtgr = doubleTapGestureRecognizer { + removeGestureRecognizer(dtgr) + doubleTapGestureRecognizer = nil + } + if enableDoubleTapForFrameReset { + installDoubleTapGestureRecognizer() + } + } + } + + /// The max distance the handle may move in any direction, where the start is the center of the joystick base and + /// the end is on the circumference of the base when travel is 1.0. + private var radius: CGFloat { return self.bounds.size.width / 2.0 * travel } /// The image to use to show the base of the joystick - private var baseImageView: UIImageView! - + private var baseImageView: UIImageView = UIImageView(image: nil) + /// The image to use to show the handle of the joystick - private var handleImageView: UIImageView! - + private var handleImageView: UIImageView = UIImageView(image: nil) + /// Cache of the last joystick angle in radians - private var lastAngleRadians: Float = 0.0 - - /// The original location of the joystick. Used to restore its position when user double-taps on it. - private var originalCenter: CGPoint? - + private var angleRadians: CGFloat = 0.0 + /// Tap gesture recognizer for double-taps which will reset the joystick position - private var tapGestureRecognizer: UITapGestureRecognizer! + private var tapGestureRecognizer: UITapGestureRecognizer? + + /// A filter for joystick base centers. Used to restrict base movements. + private var baseCenterClamper: (CGPoint) -> CGPoint = { $0 } + + /// A filter for joystick handle centers. Used to restrict handle movements. + private var handleCenterClamper: (CGPoint) -> CGPoint = { $0 } + + /// Tap gesture recognizer for detecting double-taps. Only present if `enableDoubleTapForFrameReset` is true + private var doubleTapGestureRecognizer: UITapGestureRecognizer? /** Initialize new joystick view using the given frame. @@ -105,62 +183,47 @@ public final class JoyStickView: UIView { */ public override init(frame: CGRect) { super.init(frame: frame) - initialize() } - + /** Initialize new joystick view from a file. - parameter coder: the source of the joystick configuration information */ public required init?(coder: NSCoder) { super.init(coder: coder) - initialize() } - + /** - Common initialization of view. Creates UIImageView instances for base and handle. + This is the appropriate place to configure our internal views as we have our own geometry. */ - private func initialize() { - - handleTintColor = tintColor - - baseImageView = UIImageView(image: baseImage) - baseImageView.alpha = baseAlpha - addSubview(baseImageView) - baseImageView.frame = bounds - - handleImageView = UIImageView(image: handleImage) - makeHandleImage() - addSubview(handleImageView) - handleImageView.frame = bounds.insetBy(dx: 0.15 * bounds.width, dy: 0.15 * bounds.height) - - tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(resetFrame)) - tapGestureRecognizer!.numberOfTapsRequired = 2 - addGestureRecognizer(tapGestureRecognizer!) + public override func layoutSubviews() { + super.layoutSubviews() + initialize() } - - /** - Generate a new handle image using the current `tintColor` value and install. Uses CoreImage filter to apply a - tint to the grey handle image. - */ - private func makeHandleImage() { - guard handleImageView != nil else { return } - guard let inputImage = CIImage(image: handleImage) else { - fatalError("failed to create input CIImage") - } - - let filterConfig: [String:Any] = [kCIInputIntensityKey: 1.0, - kCIInputColorKey: CIColor(color: handleTintColor!), - kCIInputImageKey: inputImage] - guard let filter = CIFilter(name: "CIColorMonochrome", parameters: filterConfig) else { - fatalError("failed to create CIFilter CIColorMonochrome") - } - - guard let outputImage = filter.outputImage else { - fatalError("failed to obtain output CIImage") +} + +// MARK: - Touch Handling + +/** + Main recognizer for movement + + We use a recognizer rather than the view's own touch handler methods as there is an iOS quirk + that delays the touchesEnded method. This quirk doesn't apply to gesture recognizers. + */ +class JoyStickViewGestureRecognizer: UIGestureRecognizer { + private var touch: UITouch? + private var firstTimestamp: TimeInterval? + private var lastTimestamp: TimeInterval? + private var firstLocation: CGPoint? + private var lastLocation: CGPoint? + + public var wasTap: Bool { + get { + if let start = firstTimestamp, let end = lastTimestamp, let startPoint = firstLocation, let lastPoint = lastLocation { + return end - start < 0.1 && max(abs(startPoint.x-lastPoint.x), abs(startPoint.y-lastPoint.y)) < 2 + } + return false } - - handleImageView.image = UIImage(ciImage: outputImage) } /** @@ -169,8 +232,10 @@ public final class JoyStickView: UIView { - parameter event: additional event info (ignored) */ public override func touchesBegan(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first else { return } - updatePosition(touch: touch) + touch = touches.first + firstTimestamp = touch?.timestamp + firstLocation = touch?.location(in: nil) + state = .began } /** @@ -179,10 +244,10 @@ public final class JoyStickView: UIView { - parameter event: additional event info (ignored) */ public override func touchesMoved(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first else { return } - updatePosition(touch: touch) + lastLocation = touch?.location(in: nil) + state = .changed } - + /** An existing touch event has been cancelled (probably due to system event such as an alert). Move joystick to center of base. @@ -190,196 +255,302 @@ public final class JoyStickView: UIView { - parameter event: additional event info (ignored) */ public override func touchesCancelled(_ touches: Set, with event: UIEvent?) { - resetPosition() + lastTimestamp = touch?.timestamp + lastLocation = touch?.location(in: nil) + state = .ended } - + /** User removed touch from display. Move joystick to center of base. - parameter touches: the set of UITouch instances, one for each touch event (ignored) - parameter event: additional event info (ignored) */ public override func touchesEnded(_ touches: Set, with event: UIEvent?) { - resetPosition() + lastTimestamp = touch?.timestamp + lastLocation = touch?.location(in: nil) + state = .ended } /** - Reset our position. + Clear state */ - @objc public func resetFrame() { - if displacement < 0.5 && originalCenter != nil { - center = originalCenter! - originalCenter = nil - } + public override func reset() { + touch = nil + firstTimestamp = nil + lastTimestamp = nil + firstLocation = nil + lastLocation = nil } /** - Reset handle position so that it is in the center of the base. + Get touch location + - parameter view: the view in which to return location coordinates + */ + public override func location(in view: UIView?) -> CGPoint { + guard touch != nil else { return .zero } + return touch!.location(in: view) + } + + /** + Get touch offset + - parameter view: the view in which to return offset coordinates */ - private func resetPosition() { - updateLocation(location: CGPoint(x: frame.midX, y: frame.midY)) + public func offset(in view: UIView?) -> CGVector { + guard let last = lastLocation, let first = firstLocation else { return .zero } + return last - first + } +} + +extension JoyStickView { + @objc private func gestureRecognizerChanged(recognizer: JoyStickViewGestureRecognizer) { + if recognizer.state == .began || recognizer.state == .changed { + handleMovement(location: recognizer.location(in: superview!), delta: recognizer.offset(in: superview!)) + } else if recognizer.state == .ended { + homePosition() + if recognizer.wasTap, let block = tappedBlock { + block() + } + } } /** - Update the handle position based on the current touch location. - - parameter touch: the UITouch instance describing where the finger/pencil is + Reset our base to the initial location before the user moved it. By default, this will take place + whenever the user double-taps on the joystick handle. */ - private func updatePosition(touch: UITouch) { - updateLocation(location: touch.location(in: superview!)) + @objc public func resetFrame() { + guard let movableCenter = self.movableCenter, displacement < 0.5 else { return } + center = movableCenter } +} + +// MARK: - Implementation Details + +extension JoyStickView { /** - Update the location of the joystick based on the given touch location. Resulting behavior depends on `movable` - setting. - - parameter location: the current handle position. NOTE: in coordinates of the superview + Common initialization of view. Creates UIImageView instances for base and handle. */ - private func updateLocation(location: CGPoint) { - guard let superview = self.superview else { return } - guard superview.bounds.contains(location) else { return } - - // Calculate displacements between given location and our frame's center - // - let delta = location - frame.mid - - // Calculate normalized displacement - // - let newDisplacement = delta.magnitude / radius -// print("********") -// print("delta.magnitude: \(delta.magnitude) radius: \(radius) newDisplacement: \(newDisplacement)") + private func initialize() { + baseImageView.frame = bounds + addSubview(baseImageView) - // Calculate pointing angle used displacements. NOTE: using this ordering of dx, dy to atan2f to obtain - // navigation angles where 0 is at top of clock dial and angle values increase in a clock-wise direction. - // - let newAngleRadians = atan2f(Float(delta.dx), Float(delta.dy)) - - if movable { - if newDisplacement > 1.0 { - - if originalCenter == nil { - originalCenter = center - } - - // Calculate point that should be on the circumference of the base image. - // - let end = CGVector(dx: CGFloat(sinf(newAngleRadians)) * radius, - dy: CGFloat(cosf(newAngleRadians)) * radius) - - // Calculate the origin of our frame, working backwards from the given location, and move to it. - // - let origin = location - end - frame.size / 2.0 - - if movableBounds != nil { - frame.origin = CGPoint(x: min(max(origin.x, movableBounds!.minX), movableBounds!.maxX - frame.width), - y: min(max(origin.y, movableBounds!.minY), movableBounds!.maxY - frame.height)) - } - else { - frame.origin = origin - } + scaleHandleImageView() + addSubview(handleImageView) + + let bundle = Bundle(for: JoyStickView.self) + + if self.baseImage == nil { + if let baseImage = UIImage(named: "DefaultBase", in: bundle, compatibleWith: nil) { + self.baseImage = baseImage } - - // Update location of handle - // - handleImageView.center = bounds.mid + delta } - else { - - // Update location of handle - // - if newDisplacement > 1.0 { - - // Keep handle on the circumference of the base image - // - let x = CGFloat(sinf(newAngleRadians)) * radius - let y = CGFloat(cosf(newAngleRadians)) * radius - handleImageView.frame.origin = CGPoint(x: x + bounds.midX - handleImageView.bounds.size.width / 2.0, - y: y + bounds.midY - handleImageView.bounds.size.height / 2.0) - } - else { - handleImageView.center = bounds.mid + delta + + baseImageView.image = baseImage + + if self.handleImage == nil { + if let handleImage = UIImage(named: "DefaultHandle", in: bundle, compatibleWith: nil) { + self.handleImage = handleImage } } - // Update joystick reporting values - // - let newClampedDisplacement = min(newDisplacement, 1.0) - if newClampedDisplacement != displacement || newAngleRadians != lastAngleRadians { - displacement = newClampedDisplacement - lastAngleRadians = newAngleRadians - - // Convert to degrees: 0° is up, 90° is right, 180° is down and 270° is left - // - self.angle = newClampedDisplacement != 0.0 ? CGFloat(180.0 - newAngleRadians * 180.0 / Float.pi) : 0.0 - // monitor?(angle, displacement) - self.delegate?.handleJoyStick(angle: angle, displacement: displacement) - - -// print("delta x: \(delta.dx) delta y: \(delta.dy)") - - let new_x = (delta.dx / (radius * 2)) - let new_y = (delta.dy / (radius * 2)) * -1 -// print("new_x: \(new_x) new_y: \(new_y)") - self.delegate?.handleJoyStickPosition(x: new_x, y: new_y) + generateHandleImage() + + addGestureRecognizer(JoyStickViewGestureRecognizer(target: self, action: #selector(gestureRecognizerChanged))) + + if enableDoubleTapForFrameReset { + installDoubleTapGestureRecognizer() } } -} -public func LiangBarsky(rect: CGRect, p0: CGPoint, p1: CGPoint) -> (p0: CGPoint, p1: CGPoint, inRect: Bool) { - let edgeLeft = rect.minX - let edgeRight = rect.maxX - let edgeBottom = rect.minY - let edgeTop = rect.maxY + private func scaleHandleImageView() { + let inset = (1.0 - handleSizeRatio) * bounds.width / 2.0 + handleImageView.frame = bounds.insetBy(dx: inset, dy: inset) + } - var t0: CGFloat = 0.0 - var t1: CGFloat = 1.0 - let xd = p1.x - p0.x - let yd = p1.y - p0.y - let cases = [(-xd, -(edgeLeft - p0.x)), - ( xd, edgeRight - p0.x), - (-yd, -(edgeBottom - p0.y)), - ( yd, edgeTop - p0.y)] + /** + Install a UITapGestureRecognizer to detect and process double-tap activity on the joystick. + */ + private func installDoubleTapGestureRecognizer() { + tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(resetFrame)) + tapGestureRecognizer!.numberOfTapsRequired = 2 + addGestureRecognizer(tapGestureRecognizer!) + } + + private func generateHandleImage() { + if colorFillHandleImage { + colorHandleImage() + } + else { + tintHandleImage() + } + } - // Check edges against the appropriate coordinate delta - // - let epsilon: CGFloat = 1.0e-8 + /** + Generate a handle image by applying the `handleTintColor` value to the handeImage + */ + private func colorHandleImage() { + guard let handleImage = self.handleImage else { return } + if let handleTintColor = self.handleTintColor { + let image = handleImage.withRenderingMode(.alwaysTemplate) + handleImageView.image = image + handleImageView.tintColor = handleTintColor + } + else { + handleImageView.tintColor = nil + handleImageView.image = handleImage + } + } - for (p, q) in cases { + private func tintHandleImage() { + guard let handleImage = self.handleImage else { return } + + guard let handleTintColor = self.handleTintColor else { + handleImageView.image = handleImage + return + } + + guard let inputImage = CIImage(image: handleImage) else { + fatalError("failed to create input CIImage") + } + + let filterConfig: [String:Any] = [kCIInputIntensityKey: 1.0, + kCIInputColorKey: CIColor(color: handleTintColor), + kCIInputImageKey: inputImage] + #if swift(>=4.2) + guard let filter = CIFilter(name: "CIColorMonochrome", parameters: filterConfig) else { + fatalError("failed to create CIFilter CIColorMonochrome") + } + #else + guard let filter = CIFilter(name: "CIColorMonochrome", withInputParameters: filterConfig) else { + fatalError("failed to create CIFilter CIColorMonochrome") + } + #endif - // Protect from explosion when calculating 'r' below with 'p' in denominator + guard let outputImage = filter.outputImage else { + fatalError("failed to obtain output CIImage") + } + + handleImageView.image = UIImage(ciImage: outputImage) + } + + /** + Reset handle position so that it is in the center of the base. + */ + private func homePosition() { + handleImageView.center = bounds.mid + reportPosition() + } + + /** + Handle joystick movement. Resulting behavior depends on `movable` setting. + - parameter location: the current handle position, in coordinates of the superview + - parameter delta: the current touch offset, in coordinates of the superview + */ + private func handleMovement(location: CGPoint, delta: CGVector) { + guard let superview = self.superview else { return } + guard superview.bounds.contains(location) else { return } + + let newDisplacement = delta.magnitude / radius + + // Calculate pointing angle used displacements. NOTE: using this ordering of dx, dy to atan2f to obtain + // navigation angles where 0 is at top of clock dial and angle values increase in a clock-wise direction. This + // also assumes that Y increases in the downward direction. // - if abs(p) < epsilon { - if q < 0.0 { - - // Horizontal or vertical line that is outside of the rectangle - // - return (p0: p0, p1: p1, inRect: false) + let newAngleRadians = atan2(delta.dx, delta.dy) + + if movable { + if newDisplacement > 1.0 && repositionBase(location: location, angle: newAngleRadians) { + repositionHandle(angle: newAngleRadians) + } + else { + handleImageView.center = handleCenterClamper(bounds.mid + delta) } } + else if newDisplacement > 1.0 { + repositionHandle(angle: newAngleRadians) + } else { + handleImageView.center = handleCenterClamper(bounds.mid + delta) + } + + reportPosition() + } + + /** + Report the current joystick values to any registered `monitor`. + */ + private func reportPosition() { + let delta = handleImageView.center - baseImageView.center + let displacement = delta.magnitude2 == 0.0 ? 0.0 : delta.magnitude / radius + let angleRadians = delta.magnitude2 == 0.0 ? 0.0 : atan2(delta.dx, delta.dy) + + self.displacement = displacement + self.angleRadians = angleRadians - // Safe to do since 'p' is not zero here. However, maybe we should do better since very small 'p' will lead - // to a very large 'r'. We can do 'q > t1 * p' for instance in the condition below, but we then need to - // change use of 't0' in the parametric equation at the bottom. - // - let r: CGFloat = q / p - if p < 0.0 { - if r > t1 { - return (p0: p0, p1: p1, inRect: false) - } - else if r > t0 { - t0 = r - } - } - else if p > 0.0 { - if r < t0 { - return (p0: p0, p1: p1, inRect: false) - } - else if r < t1 { - t1 = r - } - } + switch monitor { + case let .polar(monitor): monitor(JoyStickViewPolarReport(angle: self.angle, displacement: displacement)) + case let .xy(monitor): monitor(JoyStickViewXYReport(x: delta.dx, y: -delta.dy)) + case .none: break } } - return (p0: CGPoint(x: p0.x + t0 * xd, y: p0.y + t0 * yd), - p1: CGPoint(x: p0.x + t1 * xd, y: p0.y + t1 * yd), - inRect: true) + /** + Move the base so that the handle displacement is <= 1.0 from the base. THe last step of this operation is + a clamping of the base origin so that it stays within a configured boundary. Such clamping can result in + a joystick handle whose displacement is > 1.0 from the base, so the caller should account for that by looking + for a `true` return value. + + - parameter location: the current joystick handle center position + - parameter angle: the angle the handle makes with the center of the base + - returns: true if the base **cannot** move sufficiently to keep the displacement of the handle <= 1.0 + */ + private func repositionBase(location: CGPoint, angle: CGFloat) -> Bool { + if movableCenter == nil { + movableCenter = self.center + } + + // Calculate point that should be on the circumference of the base image. + // + let end = CGVector(dx: sin(angle) * radius, dy: cos(angle) * radius) + + // Calculate the origin of our frame, working backwards from the given location, and move to it. + // + let desiredCenter = location - end // - frame.size / 2.0 + self.center = baseCenterClamper(desiredCenter) + return self.center != desiredCenter + } + + /** + Move the joystick handle so that the angle made up of the triangle from the base 12:00 position on its + circumference, the base center and the joystick center is the given value. + + - parameter angle: the angle (radians) to conform to + */ + private func repositionHandle(angle: CGFloat) { + + // Keep handle on the circumference of the base image + // + let x = sin(angle) * radius + let y = cos(angle) * radius + handleImageView.frame.origin = CGPoint(x: x + bounds.midX - handleImageView.bounds.size.width / 2.0, + y: y + bounds.midY - handleImageView.bounds.size.height / 2.0) + + handleImageView.center = handleCenterClamper(handleImageView.center) + } +} + +/** + Provide support for Obj-C monitors by wrapping a block in a closure that works with the Swift-only types. + */ +extension JoyStickView { + + @objc public func setPolarMonitor(_ block: @escaping (CGFloat, CGFloat) -> Void) { + let bridge = {(report: JoyStickViewPolarReport) in block(report.angle, report.displacement) } + monitor = .polar(monitor: bridge) + } + + @objc public func setXYMonitor(_ block: @escaping (CGFloat, CGFloat) -> Void) { + let bridge = {(report: JoyStickViewXYReport) in block(report.x, report.y) } + monitor = .xy(monitor: bridge) + } } diff --git a/Quake2-iOS/JoyStickViewMonitor.swift b/Quake2-iOS/JoyStickViewMonitor.swift new file mode 100644 index 0000000..19f2091 --- /dev/null +++ b/Quake2-iOS/JoyStickViewMonitor.swift @@ -0,0 +1,93 @@ +// Copyright © 2020 Brad Howes. All rights reserved. + +import CoreGraphics + +/** + JoyStickView handle position as X, Y deltas from the base center. Note that here a positive `y` indicates that the + joystick handle is pushed upwards. + */ +public struct JoyStickViewXYReport { + /// Delta X of handle from base center + public let x: CGFloat + /// Delta Y of handle from base center + public let y: CGFloat + + /** + Constructor of new XY report + + - parameter x: X offset from center of the base + - parameter y: Y offset from center of the base (positive values towards up/north) + */ + public init(x: CGFloat, y: CGFloat) { + self.x = x + self.y = y + } + + /// Convert this report into polar format + public var polar: JoyStickViewPolarReport { + return JoyStickViewPolarReport(angle: (180.0 - atan2(x, -y) * 180.0 / .pi), displacement: sqrt(x * x + y * y)) + } +} + +/** + JoyStickView handle position as angle/displacement values from the base center. Note that `angle` is given in degrees, + with 0° pointing up (north) and 90° pointing right (east). + */ +public struct JoyStickViewPolarReport { + /// Clockwise angle of the handle with respect to north/up of 0°. + public let angle: CGFloat + /// Distance from the center of the base + public let displacement: CGFloat + + /** + Constructor of new polar report + + - parameter angle: clockwise angle of the handle with respect to north/up of 0°. + - parameter displacement: distance from the center of the base + */ + public init(angle: CGFloat, displacement: CGFloat) { + self.angle = angle + self.displacement = displacement + } + + /// Convert this report into XY format + public var rectangular: JoyStickViewXYReport { + let rads = angle * .pi / 180.0 + return JoyStickViewXYReport(x: sin(rads) * displacement, y: cos(rads) * displacement) + } +} + +/** + Prototype of a monitor function that accepts a JoyStickViewXYReport. + */ +public typealias JoyStickViewXYMonitor = (_ value: JoyStickViewXYReport) -> Void + +/** + Prototype of a monitor function that accepts a JoyStickViewXYReport. + */ +public typealias JoyStickViewPolarMonitor = (_ value: JoyStickViewPolarReport) -> Void + +/** + Monitor kind. Determines the type of reporting that will be emitted from a JoyStickView instance. + */ +public enum JoyStickViewMonitorKind { + + /** + Install monitor that accepts polar position change reports + + - parameter monitor: function that accepts a JoyStickViewPolarReport + */ + case polar(monitor: JoyStickViewPolarMonitor) + + /** + Install monitor that accepts cartesian (XY) position change reports + + - parameter monitor: function that accepts a JoyStickViewXYReport + */ + case xy(monitor: JoyStickViewXYMonitor) + + /** + No monitoring for a JoyStickView instance. + */ + case none +} diff --git a/Quake2-iOS/Main.storyboard b/Quake2-iOS/Main.storyboard index 607966b..1839bf1 100644 --- a/Quake2-iOS/Main.storyboard +++ b/Quake2-iOS/Main.storyboard @@ -1,11 +1,8 @@ - - - - + + - - + @@ -30,7 +27,7 @@ - + - - - - + @@ -113,7 +111,6 @@ - @@ -148,6 +145,7 @@ + @@ -157,7 +155,6 @@ - @@ -206,8 +203,8 @@ - + @@ -228,7 +226,6 @@ - @@ -253,7 +250,7 @@ - - - - + @@ -392,7 +391,6 @@ - @@ -406,10 +404,10 @@ - - - + + + diff --git a/Quake2-iOS/SDL_uikitviewcontroller+Additions.swift b/Quake2-iOS/SDL_uikitviewcontroller+Additions.swift index 7979370..13e65ce 100644 --- a/Quake2-iOS/SDL_uikitviewcontroller+Additions.swift +++ b/Quake2-iOS/SDL_uikitviewcontroller+Additions.swift @@ -7,6 +7,10 @@ import UIKit +func applyJoystickCurve(position: CGFloat, range: CGFloat) -> Float { + return Float(pow(position / range, 2) * range * (position < 0 ? -1 : 1)) +} + extension SDL_uikitviewcontroller { // A method of getting around the fact that Swift extensions cannot have stored properties @@ -14,7 +18,8 @@ extension SDL_uikitviewcontroller { struct Holder { static var _fireButton = UIButton() static var _jumpButton = UIButton() - static var _joystickView = JoyStickView(frame: .zero) + static var _leftJoystickView = JoyStickView(frame: .zero) + static var _rightJoystickView = JoyStickView(frame: .zero) static var _tildeButton = UIButton() static var _expandButton = UIButton() static var _escapeButton = UIButton() @@ -45,12 +50,21 @@ extension SDL_uikitviewcontroller { } } - var joystickView:JoyStickView { + var leftJoystickView:JoyStickView { get { - return Holder._joystickView + return Holder._leftJoystickView } set(newValue) { - Holder._joystickView = newValue + Holder._leftJoystickView = newValue + } + } + + var rightJoystickView:JoyStickView { + get { + return Holder._rightJoystickView + } + set(newValue) { + Holder._rightJoystickView = newValue } } @@ -145,8 +159,9 @@ extension SDL_uikitviewcontroller { } @objc func fireButton(rect: CGRect) -> UIButton { - fireButton = UIButton(frame: CGRect(x: rect.width - 155, y: rect.height - 90, width: 75, height: 75)) + fireButton = UIButton(frame: CGRect(x: rect.width - 205, y: rect.height - 150, width: 50, height: 50)) fireButton.setTitle("FIRE", for: .normal) + fireButton.titleLabel?.font = UIFont.systemFont(ofSize: 12) fireButton.setBackgroundImage(UIImage(named: "JoyStickBase")!, for: .normal) fireButton.addTarget(self, action: #selector(self.firePressed), for: .touchDown) fireButton.addTarget(self, action: #selector(self.fireReleased), for: .touchUpInside) @@ -155,8 +170,9 @@ extension SDL_uikitviewcontroller { } @objc func jumpButton(rect: CGRect) -> UIButton { - jumpButton = UIButton(frame: CGRect(x: rect.width - 90, y: rect.height - 135, width: 75, height: 75)) + jumpButton = UIButton(frame: CGRect(x: rect.width - 150, y: rect.height - 205, width: 50, height: 50)) jumpButton.setTitle("JUMP", for: .normal) + jumpButton.titleLabel?.font = UIFont.systemFont(ofSize: 12) jumpButton.setBackgroundImage(UIImage(named: "JoyStickBase")!, for: .normal) jumpButton.addTarget(self, action: #selector(self.jumpPressed), for: .touchDown) jumpButton.addTarget(self, action: #selector(self.jumpReleased), for: .touchUpInside) @@ -164,19 +180,71 @@ extension SDL_uikitviewcontroller { return jumpButton } - @objc func joyStick(rect: CGRect) -> JoyStickView { + @objc func leftJoyStick(rect: CGRect) -> JoyStickView { let size = CGSize(width: 100.0, height: 100.0) - let joystick1Frame = CGRect(origin: CGPoint(x: 50.0, - y: (rect.height - size.height - 50.0)), + let frame = CGRect(origin: CGPoint(x: 50.0, + y: (rect.height - size.height - 50.0)), size: size) - joystickView = JoyStickView(frame: joystick1Frame) - joystickView.delegate = self + leftJoystickView = JoyStickView(frame: frame) + leftJoystickView.monitor = .xy(monitor: { report in + + cl_joyscale.strafe = applyJoystickCurve(position: abs(report.x), range: size.width/2) * 5 + if report.x > 0 { + Key_Event(Int32(Character("d").asciiValue!), qboolean(1), qboolean(1)) + Key_Event(Int32(Character("a").asciiValue!), qboolean(0), qboolean(1)) + } else if report.x < 0 { + Key_Event(Int32(Character("d").asciiValue!), qboolean(0), qboolean(1)) + Key_Event(Int32(Character("a").asciiValue!), qboolean(1), qboolean(1)) + } else { + cl_joyscale.strafe = 0 + Key_Event(Int32(Character("d").asciiValue!), qboolean(0), qboolean(1)) + Key_Event(Int32(Character("a").asciiValue!), qboolean(0), qboolean(1)) + } + + cl_joyscale.walk = applyJoystickCurve(position: abs(report.y), range: size.width/2) * 2 + if report.y > 0 { + Key_Event(Int32(K_UPARROW.rawValue), qboolean(1), qboolean(1)) + Key_Event(Int32(K_DOWNARROW.rawValue), qboolean(0), qboolean(1)) + } else if report.y < 0 { + Key_Event(Int32(K_UPARROW.rawValue), qboolean(0), qboolean(1)) + Key_Event(Int32(K_DOWNARROW.rawValue), qboolean(1), qboolean(1)) + } else { + cl_joyscale.walk = 0 + Key_Event(Int32(K_UPARROW.rawValue), qboolean(0), qboolean(1)) + Key_Event(Int32(K_DOWNARROW.rawValue), qboolean(0), qboolean(1)) + } + }) - joystickView.movable = false - joystickView.alpha = 0.5 - joystickView.baseAlpha = 0.5 // let the background bleed thru the base - joystickView.handleTintColor = UIColor.darkGray // Colorize the handle - return joystickView + leftJoystickView.movable = false + leftJoystickView.alpha = 0.75 + leftJoystickView.baseAlpha = 0.25 // let the background bleed thru the base + leftJoystickView.baseImage = UIImage(named: "JoyStickBase") + leftJoystickView.handleImage = UIImage(named: "JoyStickHandle") + return leftJoystickView + } + + @objc func rightJoyStick(rect: CGRect) -> JoyStickView { + let size = CGSize(width: 100.0, height: 100.0) + let frame = CGRect(origin: CGPoint(x: (rect.width - size.width - 50.0), + y: (rect.height - size.height - 50.0)), + size: size) + rightJoystickView = JoyStickView(frame: frame) + rightJoystickView.monitor = .xy(monitor: { report in + cl_joyscale.yaw = Float(applyJoystickCurve(position: report.x, range: size.width/2) * 0.1) + cl_joyscale.pitch = Float(applyJoystickCurve(position: report.y, range: size.width/2) * 0.05) + }) + + rightJoystickView.tappedBlock = { + Key_Event(Int32(K_CTRL.rawValue), qboolean(1), qboolean(1)) + Key_Event(Int32(K_CTRL.rawValue), qboolean(0), qboolean(1)) + } + + rightJoystickView.movable = false + rightJoystickView.alpha = 0.75 + rightJoystickView.baseAlpha = 0.25 // let the background bleed thru the base + rightJoystickView.baseImage = UIImage(named: "JoyStickBase") + rightJoystickView.handleImage = UIImage(named: "JoyStickHandle") + return rightJoystickView } @objc func buttonStack(rect: CGRect) -> UIStackView { @@ -268,75 +336,75 @@ extension SDL_uikitviewcontroller { @objc func firePressed(sender: UIButton!) { - Key_Event(137, qboolean(1), qboolean(1)) + Key_Event(Int32(K_CTRL.rawValue), qboolean(1), qboolean(1)) } @objc func fireReleased(sender: UIButton!) { - Key_Event(137, qboolean(0), qboolean(1)) + Key_Event(Int32(K_CTRL.rawValue), qboolean(0), qboolean(1)) } @objc func jumpPressed(sender: UIButton!) { - Key_Event(32, qboolean(1), qboolean(1)) + Key_Event(Int32(K_SPACE.rawValue), qboolean(1), qboolean(1)) } @objc func jumpReleased(sender: UIButton!) { - Key_Event(32, qboolean(0), qboolean(1)) + Key_Event(Int32(K_SPACE.rawValue), qboolean(0), qboolean(1)) } @objc func tildePressed(sender: UIButton!) { -// Key_Event(32, qboolean(1), qboolean(1)) +// Key_Event(Int32(K_SPACE.rawValue), qboolean(1), qboolean(1)) } @objc func tildeReleased(sender: UIButton!) { -// Key_Event(32, qboolean(0), qboolean(1)) +// Key_Event(Int32(K_SPACE.rawValue), qboolean(0), qboolean(1)) } @objc func escapePressed(sender: UIButton!) { - Key_Event(27, qboolean(1), qboolean(1)) + Key_Event(Int32(K_ESCAPE.rawValue), qboolean(1), qboolean(1)) } @objc func escapeReleased(sender: UIButton!) { - Key_Event(27, qboolean(0), qboolean(1)) + Key_Event(Int32(K_ESCAPE.rawValue), qboolean(0), qboolean(1)) } @objc func quickSavePressed(sender: UIButton!) { - Key_Event(150, qboolean(1), qboolean(1)) + Key_Event(Int32(K_F6.rawValue), qboolean(1), qboolean(1)) } @objc func quickSaveReleased(sender: UIButton!) { - Key_Event(150, qboolean(0), qboolean(1)) + Key_Event(Int32(K_F6.rawValue), qboolean(0), qboolean(1)) } @objc func quickLoadPressed(sender: UIButton!) { - Key_Event(153, qboolean(1), qboolean(1)) + Key_Event(Int32(K_F9.rawValue), qboolean(1), qboolean(1)) } @objc func quickLoadReleased(sender: UIButton!) { - Key_Event(153, qboolean(0), qboolean(1)) + Key_Event(Int32(K_F9.rawValue), qboolean(0), qboolean(1)) } @objc func f1Pressed(sender: UIButton!) { - Key_Event(145, qboolean(1), qboolean(1)) + Key_Event(Int32(K_F1.rawValue), qboolean(1), qboolean(1)) } @objc func f1Released(sender: UIButton!) { - Key_Event(145, qboolean(0), qboolean(1)) + Key_Event(Int32(K_F1.rawValue), qboolean(0), qboolean(1)) } @objc func prevWeaponPressed(sender: UIButton!) { - Key_Event(183, qboolean(1), qboolean(1)) + Key_Event(Int32(K_MWHEELDOWN.rawValue), qboolean(1), qboolean(1)) } @objc func prevWeaponReleased(sender: UIButton!) { - Key_Event(183, qboolean(0), qboolean(1)) + Key_Event(Int32(K_MWHEELDOWN.rawValue), qboolean(0), qboolean(1)) } @objc func nextWeaponPressed(sender: UIButton!) { - Key_Event(184, qboolean(1), qboolean(1)) + Key_Event(Int32(K_MWHEELUP.rawValue), qboolean(1), qboolean(1)) } @objc func nextWeaponReleased(sender: UIButton!) { - Key_Event(184, qboolean(0), qboolean(1)) + Key_Event(Int32(K_MWHEELUP.rawValue), qboolean(0), qboolean(1)) } @@ -359,31 +427,3 @@ extension SDL_uikitviewcontroller { } } - -extension SDL_uikitviewcontroller: JoystickDelegate { - - func handleJoyStickPosition(x: CGFloat, y: CGFloat) { - - if y > 0 { - cl_joyscale_y.0 = Int32(abs(y) * 60) - Key_Event(132, qboolean(1), qboolean(1)) - Key_Event(133, qboolean(0), qboolean(1)) - } else if y < 0 { - cl_joyscale_y.1 = Int32(abs(y) * 60) - Key_Event(132, qboolean(0), qboolean(1)) - Key_Event(133, qboolean(1), qboolean(1)) - } else { - cl_joyscale_y.0 = 0 - cl_joyscale_y.1 = 0 - Key_Event(132, qboolean(0), qboolean(1)) - Key_Event(133, qboolean(0), qboolean(1)) - } - - cl_joyscale_x.0 = Int32(x * 20) - } - - func handleJoyStick(angle: CGFloat, displacement: CGFloat) { -// print("angle: \(angle) displacement: \(displacement)") - } - -} diff --git a/Quake2-iOS/SavedGameViewController.swift b/Quake2-iOS/SavedGameViewController.swift index 491f15f..cb937e1 100644 --- a/Quake2-iOS/SavedGameViewController.swift +++ b/Quake2-iOS/SavedGameViewController.swift @@ -69,15 +69,6 @@ extension SavedGameViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { selectedSavedGame = saves[indexPath.row] loadGameButton.isHidden = false - #if os(tvOS) - tableView.cellForRow(at: indexPath)?.contentView.backgroundColor = .lightGray - #endif - } - - func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { - #if os(tvOS) - tableView.cellForRow(at: indexPath)?.contentView.backgroundColor = .none - #endif } // func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { @@ -95,9 +86,11 @@ extension SavedGameViewController: UITableViewDataSource { let cell = tableView.dequeueReusableCell(withIdentifier: "cell")! cell.textLabel?.text = saves[indexPath.row]//.replacingOccurrences(of: ".svg", with: "") - #if os(tvOS) - cell.textLabel?.textColor = .black - #endif + cell.textLabel?.textColor = .white + cell.backgroundColor = .clear + cell.selectedBackgroundView = UIView.init(frame: .zero) + cell.selectedBackgroundView?.backgroundColor = UIColor.init(white: 1, alpha: 0.2) + cell.selectedBackgroundView?.layer.cornerRadius = 10 return cell } diff --git a/Quake2-tvOS/Main.storyboard b/Quake2-tvOS/Main.storyboard index de9c7d5..7c5eaea 100644 --- a/Quake2-tvOS/Main.storyboard +++ b/Quake2-tvOS/Main.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -34,7 +32,7 @@ - + - - - - - + @@ -306,7 +306,6 @@ - @@ -318,10 +317,10 @@ + + + - - - diff --git a/Quake2/client/cl_input.c b/Quake2/client/cl_input.c index 435d8f5..9375f63 100644 --- a/Quake2/client/cl_input.c +++ b/Quake2/client/cl_input.c @@ -472,18 +472,19 @@ CL_BaseMove(usercmd_t *cmd) cmd->sidemove -= cl_sidespeed->value * CL_KeyState(&in_left); } - cmd->sidemove += cl_joyscale_x[0] * 2.0f * CL_KeyState(&in_moveright); - cmd->sidemove -= cl_joyscale_x[1] * 2.0f * CL_KeyState(&in_moveleft); + cmd->sidemove += cl_joyscale.strafe * 2.0f * CL_KeyState(&in_moveright); + cmd->sidemove -= cl_joyscale.strafe * 2.0f * CL_KeyState(&in_moveleft); - cl.viewangles[YAW] -= cl_joyscale_x[0]; + cl.viewangles[YAW] -= cl_joyscale.yaw; + cl.viewangles[PITCH] -= cl_joyscale.pitch; cmd->upmove = cl_upspeed->value * CL_KeyState(&in_up); cmd->upmove -= cl_upspeed->value * CL_KeyState(&in_down); if (!(in_klook.state & 1)) { - cmd->forwardmove += cl_joyscale_y[0] * 4.0f * CL_KeyState(&in_forward); - cmd->forwardmove -= cl_joyscale_y[1] * 4.0f * CL_KeyState(&in_back); + cmd->forwardmove += cl_joyscale.walk * 4.0f * CL_KeyState(&in_forward); + cmd->forwardmove -= cl_joyscale.walk * 4.0f * CL_KeyState(&in_back); } #else if (in_strafe.state & 1) diff --git a/Quake2/client/header/client.h b/Quake2/client/header/client.h index b15bdc8..f651c74 100644 --- a/Quake2/client/header/client.h +++ b/Quake2/client/header/client.h @@ -256,8 +256,12 @@ extern client_static_t cls; extern int num_power_sounds; #ifdef IOS -int cl_joyscale_x[2]; -int cl_joyscale_y[2]; +struct cl_joyscale_t { + float yaw; + float pitch; + float walk; + float strafe; +} cl_joyscale; #endif /* cvars */ diff --git a/Quake2/client/vid/glimp_sdl.m b/Quake2/client/vid/glimp_sdl.m index 95ad365..64135c6 100644 --- a/Quake2/client/vid/glimp_sdl.m +++ b/Quake2/client/vid/glimp_sdl.m @@ -380,7 +380,8 @@ initialized rendering backend GLimp_InitGraphics() is also [rootVC.view addSubview:[rootVC fireButtonWithRect:[rootVC.view frame]]]; [rootVC.view addSubview:[rootVC jumpButtonWithRect:[rootVC.view frame]]]; - [rootVC.view addSubview:[rootVC joyStickWithRect:[rootVC.view frame]]]; + [rootVC.view addSubview:[rootVC leftJoyStickWithRect:[rootVC.view frame]]]; + [rootVC.view addSubview:[rootVC rightJoyStickWithRect:[rootVC.view frame]]]; [rootVC.view addSubview:[rootVC buttonStackWithRect:[rootVC.view frame]]]; [rootVC.view addSubview:[rootVC f1ButtonWithRect:[rootVC.view frame]]]; [rootVC.view addSubview:[rootVC prevWeaponButtonWithRect:[rootVC.view frame]]];