@@ -19,7 +19,7 @@ import GameplayKit
1919@available ( macOS 10 . 15 , * )
2020public final class KeyboardControlledRotationComponent : OctopusComponent , OctopusUpdatableComponent {
2121
22- // TODO: TimeStep options
22+ // TODO: Tests
2323 // TODO: Reset the acceleration when the direction reverses, as that is more natural.
2424
2525 public override var requiredComponents : [ GKComponent . Type ] ? {
@@ -28,61 +28,117 @@ public final class KeyboardControlledRotationComponent: OctopusComponent, Octopu
2828 }
2929
3030 /// Change this to a different code to customize the keys.
31- public var arrowRight : UInt16 = . arrowRight
31+ public var arrowRight : UInt16 = . arrowRight
32+
3233 /// Change this to a different code to customize the keys.
33- public var arrowLeft : UInt16 = . arrowLeft
34+ public var arrowLeft : UInt16 = . arrowLeft
35+
36+ /// The amount to rotate the node by in a single update, with optional acceleration.
37+ public var radiansPerUpdate : AcceleratedValue < CGFloat >
38+
39+ /// Specifies a fixed or variable timestep for per-update changes.
40+ public var timestep : TimeStep
3441
35- /// The minimum amount to rotate the node by in a single second.
36- public var baseRadiansPerSecond : CGFloat
42+ /// If `true`, `radiansPerUpdate` is reset to its base value when there is no rotation, for realistic inertia.
43+ ///
44+ /// `radiansPerUpdate` is always reset when there is no player input.
45+ public var resetAccelerationWhenChangingDirection : Bool
3746
38- public var maximumRadiansPerSecond : CGFloat
39- public var acceleratedRadians : CGFloat = 0
40- public var accelerationPerSecond : CGFloat
47+ /// Records the previous direction for use with `resetAccelerationWhenChangingDirection`, where `1` is counter-clockwise, `-1` is clockwise, and `0` is stationary.
48+ public var directionForPreviousFrame : Int = 0 // Not private(set) so update(deltaTime:) can be @inlinable
4149
42- public init ( baseRadiansPerSecond : CGFloat = 2.0 , // ÷ 60 per frame
43- maximumRadiansPerSecond : CGFloat = 4.0 ,
44- accelerationPerSecond : CGFloat = 2.0 )
50+ public init ( radiansPerUpdate : AcceleratedValue < CGFloat > ,
51+ timestep : TimeStep = . perSecond ,
52+ resetAccelerationWhenChangingDirection : Bool = true )
4553 {
46- self . baseRadiansPerSecond = baseRadiansPerSecond
47- self . maximumRadiansPerSecond = maximumRadiansPerSecond
48- self . accelerationPerSecond = accelerationPerSecond
54+ self . radiansPerUpdate = radiansPerUpdate
55+ self . timestep = timestep
56+ self . resetAccelerationWhenChangingDirection = resetAccelerationWhenChangingDirection
4957 super. init ( )
5058 }
59+
60+ public convenience init ( radiansPerUpdate: CGFloat = 1.0 , // ÷ 60 = 0.01666666667 per frame
61+ acceleration: CGFloat = 0 ,
62+ maximum: CGFloat = 1.0 ,
63+ timestep: TimeStep = . perSecond,
64+ resetAccelerationWhenChangingDirection: Bool = true )
65+ {
66+ self . init ( radiansPerUpdate: AcceleratedValue < CGFloat > ( base: radiansPerUpdate,
67+ current: radiansPerUpdate,
68+ maximum: maximum,
69+ minimum: 0 ,
70+ acceleration: acceleration) ,
71+ timestep: timestep,
72+ resetAccelerationWhenChangingDirection: resetAccelerationWhenChangingDirection)
73+ }
5174
5275 public required init ? ( coder aDecoder: NSCoder ) { fatalError ( " init(coder:) has not been implemented " ) }
5376
5477 @inlinable
5578 public override func update( deltaTime seconds: TimeInterval ) {
5679
80+ // #0: If there is no input for this frame, reset the acceleration and exit.
81+
5782 guard
5883 let keyboardEventComponent = coComponent ( KeyboardEventComponent . self) ,
5984 !keyboardEventComponent. codesPressed. isEmpty,
6085 let node = entityNode
6186 else {
62- acceleratedRadians = baseRadiansPerSecond // TODO: PERFORMANCE: Figure out a better way than setting this every frame.
87+ // TODO: PERFORMANCE: Figure out a better way than setting these every frame.
88+ radiansPerUpdate. reset ( )
89+ directionForPreviousFrame = 0
6390 return
6491 }
6592
66- // Did player press a directional arrow key?
93+ // #1: Did player press a directional arrow key?
94+
6795 // ❕ NOTE: Don't use `switch` or `else` because we want to process multiple keypresses, to cancel out opposing directions.
6896 // ❕ NOTE: Positive rotation = counter-clockwise :)
6997
7098 let codesPressed = keyboardEventComponent. codesPressed
71- let radiansForCurrentFrame = acceleratedRadians * CGFloat( seconds)
99+ var directionForCurrentFrame : Int = 0
100+
101+ if codesPressed. contains ( self . arrowRight) { directionForCurrentFrame += 1 } // ➡️
102+ if codesPressed. contains ( self . arrowLeft) { directionForCurrentFrame -= 1 } // ➡️
103+
104+ // #2: Reset the acceleration if the player changed the direction between updates.
105+
106+ if resetAccelerationWhenChangingDirection,
107+ self . directionForPreviousFrame != directionForCurrentFrame
108+ {
109+ radiansPerUpdate. reset ( )
110+ }
111+
112+ self . directionForPreviousFrame = directionForCurrentFrame
113+
114+ // #3: Exit if multiple directional inputs cancel each other out, this prevents accumulation of acceleration when there is no movement.
115+
116+ guard directionForCurrentFrame != 0 else { return }
117+
118+ // #4: Apply the rotation.
119+
120+ let radiansForCurrentFrame = timestep. applying ( radiansPerUpdate. current, deltaTime: CGFloat ( seconds) )
72121 var rotationAmountForCurrentFrame : CGFloat = 0
73122
74- if codesPressed. contains ( self . arrowRight) { rotationAmountForCurrentFrame -= radiansForCurrentFrame } // ➡️
75- if codesPressed. contains ( self . arrowLeft) { rotationAmountForCurrentFrame += radiansForCurrentFrame } // ⬅️
123+ if codesPressed. contains ( self . arrowRight) { // ➡️
124+ rotationAmountForCurrentFrame -= radiansForCurrentFrame
125+ }
126+
127+ if codesPressed. contains ( self . arrowLeft) { // ⬅️
128+ rotationAmountForCurrentFrame += radiansForCurrentFrame
129+ }
76130
77131 node. zRotation += rotationAmountForCurrentFrame
78132
79- // Apply acceleration for the next frame.
133+ #if LOGINPUTEVENTS
134+ debugLog ( " node.zRotation = \( node. zRotation) , rotationAmountForCurrentFrame = \( rotationAmountForCurrentFrame) , radiansPerUpdate = \( radiansPerUpdate) , \( timestep) " )
135+ #endif
136+
137+ // #5: Apply any acceleration, and clamp the speed to the pre-specified bounds.
80138
81- if acceleratedRadians < maximumRadiansPerSecond {
82- acceleratedRadians += ( accelerationPerSecond * CGFloat( seconds) )
83- if acceleratedRadians > maximumRadiansPerSecond {
84- acceleratedRadians = maximumRadiansPerSecond
85- }
139+ if radiansPerUpdate. isWithinBounds { // CHECK: PERFORMANCE
140+ radiansPerUpdate. update ( timestep: timestep, deltaTime: CGFloat ( seconds) )
141+ radiansPerUpdate. clamp ( )
86142 }
87143 }
88144}
0 commit comments