@@ -19,8 +19,8 @@ import GameplayKit
1919@available ( macOS 10 . 15 , * )
2020public final class KeyboardControlledTorqueComponent : OctopusComponent , OctopusUpdatableComponent {
2121
22- // TODO: TimeStep options
23- // TODO: Reset the acceleration when the direction reverses, as that is more natural.
22+ // TODO: Tests
23+ // TODO: Improve the feel
2424 // TODO: Move `maximumAngularVelocity` to `PhysicsComponent`
2525
2626 public override var requiredComponents : [ GKComponent . Type ] ? {
@@ -33,74 +33,93 @@ public final class KeyboardControlledTorqueComponent: OctopusComponent, OctopusU
3333 /// Change this to a different code to customize the keys.
3434 public var arrowLeft : UInt16 = . arrowLeft
3535
36- /// The minimum amount to rotate the node by in a single second .
37- public var baseMagnitudePerSecond : CGFloat
36+ /// The torque in Newton-meters to apply to the node every update, with optional acceleration. Affected by `timestep` .
37+ public var torquePerUpdate : AcceleratedValue < CGFloat >
3838
39- public var maximumMagnitudePerSecond : CGFloat
40- public var acceleratedMagnitude : CGFloat = 0
41- public var accelerationPerSecond : CGFloat
42- public var maximumAngularVelocity : CGFloat
39+ /// Specifies a fixed or variable timestep for per-update changes.
40+ public var timestep : TimeStep
4341
44- public init ( baseMagnitudePerSecond: CGFloat = 1.0 , // ÷ 60 per frame
45- maximumMagnitudePerSecond: CGFloat = 1.0 ,
46- maximumAngularVelocity: CGFloat = 2.0 ,
47- accelerationPerSecond: CGFloat = 0 )
42+ /// - Parameters:
43+ /// - torquePerUpdate: The amount of torque to apply every update, with optional acceleration. Affected by `timestep`.
44+ /// - timestep: Specifies a fixed or variable timestep for per-update changes. Default: `.perSecond`
45+ public init ( torquePerUpdate: AcceleratedValue < CGFloat > ,
46+ maximumAngularVelocity: CGFloat = 2.0 ,
47+ timestep: TimeStep = . perSecond)
4848 {
49- self . baseMagnitudePerSecond = baseMagnitudePerSecond
50- self . maximumMagnitudePerSecond = maximumMagnitudePerSecond
51- self . maximumAngularVelocity = maximumAngularVelocity
52- self . accelerationPerSecond = accelerationPerSecond
49+ self . torquePerUpdate = torquePerUpdate
50+ self . timestep = timestep
5351 super. init ( )
5452 }
5553
54+ /// - Parameters:
55+ /// - torquePerUpdate: The torque in Newton-meters to apply to the physics body every second. Affected by `timestep`.
56+ /// - acceleration: The amount to increase the torque by per second, while there is keyboard input. The torque is reset to the `torquePerUpdate` when there is no keyboard input. Affected by `timestep`.
57+ /// - maximum: The maximum torque to allow after acceleration has been applied.
58+ /// - timestep: Specifies a fixed or variable timestep for per-update changes. Default: `.perSecond`
59+ public convenience init ( torquePerUpdate: CGFloat = 1.0 , // ÷ 60 per frame
60+ acceleration: CGFloat = 0 ,
61+ maximum: CGFloat = 1.0 , // ÷ 60 per frame
62+ timestep: TimeStep = . perSecond)
63+ {
64+ self . init ( torquePerUpdate: AcceleratedValue < CGFloat > ( base: torquePerUpdate,
65+ current: torquePerUpdate,
66+ maximum: maximum,
67+ minimum: 0 ,
68+ acceleration: acceleration) ,
69+ timestep: timestep)
70+ }
71+
5672 public required init ? ( coder aDecoder: NSCoder ) { fatalError ( " init(coder:) has not been implemented " ) }
5773
5874 @inlinable
5975 public override func update( deltaTime seconds: TimeInterval ) {
6076
77+ // #0: If there is no input or valid entity for this frame, reset the acceleration and exit.
78+
6179 guard
6280 let keyboardEventComponent = coComponent ( KeyboardEventComponent . self) ,
6381 !keyboardEventComponent. codesPressed. isEmpty,
6482 let physicsBody = coComponent ( PhysicsComponent . self) ? . physicsBody ?? entityNode? . physicsBody
6583 else {
66- acceleratedMagnitude = baseMagnitudePerSecond // TODO: PERFORMANCE: Figure out a better way than setting this every frame.
84+ torquePerUpdate . reset ( ) // TODO: PERFORMANCE: Figure out a better way than setting this every frame.
6785 return
6886 }
6987
70- // Did player press a directional arrow key?
88+ // #1: Did player press a directional arrow key?
89+
7190 // ❕ NOTE: Don't use `switch` or `else` because we want to process multiple keypresses, to cancel out opposing directions.
7291 // ❕ NOTE: Positive rotation = counter-clockwise :)
7392
74- let codesPressed = keyboardEventComponent. codesPressed
75- let magnitudeForCurrentFrame = acceleratedMagnitude * CGFloat( seconds)
76- let currentAngularVelocity = physicsBody. angularVelocity
77- var torqueForCurrentFrame : CGFloat = 0
93+ let codesPressed = keyboardEventComponent. codesPressed
94+ let torqueForCurrentFrame = timestep. applying ( torquePerUpdate. current, deltaTime: CGFloat ( seconds) )
95+ var torqueToApply : CGFloat = 0
96+
97+ if codesPressed. contains ( self . arrowRight) { torqueToApply -= torqueForCurrentFrame } // ➡️
98+ if codesPressed. contains ( self . arrowLeft) { torqueToApply += torqueForCurrentFrame } // ⬅️
7899
79- if codesPressed. contains ( self . arrowRight) { torqueForCurrentFrame -= magnitudeForCurrentFrame } // ➡️
80- if codesPressed. contains ( self . arrowLeft) { torqueForCurrentFrame += magnitudeForCurrentFrame } // ⬅️
100+ // #2: Exit if multiple directional inputs cancel each other out, this prevents accumulation of acceleration when there is no movement.
81101
82- if abs ( currentAngularVelocity) < maximumAngularVelocity {
83- physicsBody. applyTorque ( torqueForCurrentFrame)
102+ guard torqueToApply != 0 else {
103+ torquePerUpdate. reset ( )
104+ #if LOGINPUTEVENTS
105+ debugLog ( " torquePerUpdate: \( torquePerUpdate) , torqueForCurrentFrame: \( torqueForCurrentFrame) , torqueToApply: \( torqueToApply) , angularVelocity: \( physicsBody. angularVelocity) " )
106+ #endif
107+ return
84108 }
85109
86- // Limit the body's maximum angular velocity .
110+ // #3: Apply the final torque to the physics body .
87111
88- if abs ( currentAngularVelocity) > maximumAngularVelocity {
89- // CHECK: Find a better way?
90- physicsBody. angularVelocity = maximumAngularVelocity * CGFloat( sign ( Float ( physicsBody. angularVelocity) ) )
91- }
112+ physicsBody. applyTorque ( torqueToApply)
92113
93114 #if LOGINPUTEVENTS
94- debugLog ( " acceleratedMagnitude : \( acceleratedMagnitude ) , magnitudeForCurrentFrame : \( magnitudeForCurrentFrame ) , torqueForCurrentFrame : \( torqueForCurrentFrame ) , angularVelocity: \( physicsBody. angularVelocity) " )
115+ debugLog ( " torquePerUpdate : \( torquePerUpdate ) , torqueForCurrentFrame : \( torqueForCurrentFrame ) , torqueToApply : \( torqueToApply ) , angularVelocity: \( physicsBody. angularVelocity) " )
95116 #endif
96117
97- // Apply acceleration for the next frame.
118+ // #4: Apply acceleration for the next frame.
98119
99- if acceleratedMagnitude < maximumMagnitudePerSecond {
100- acceleratedMagnitude += ( accelerationPerSecond * CGFloat( seconds) )
101- if acceleratedMagnitude > maximumMagnitudePerSecond {
102- acceleratedMagnitude = maximumMagnitudePerSecond
103- }
120+ if torquePerUpdate. isWithinBounds { // CHECK: PERFORMANCE
121+ torquePerUpdate. update ( timestep: timestep, deltaTime: CGFloat ( seconds) )
122+ torquePerUpdate. clamp ( )
104123 }
105124 }
106125}
0 commit comments