Skip to content

Commit a0aa210

Browse files
committed
Update KeyboardControlledTorqueComponent.swift
- Adopt AcceleratedValue - Add TimeStep options - Improve acceleration logic
1 parent f09259d commit a0aa210

File tree

1 file changed

+57
-38
lines changed

1 file changed

+57
-38
lines changed

Sources/OctopusKit/Components/Input/Keyboard/KeyboardControlledTorqueComponent.swift

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import GameplayKit
1919
@available(macOS 10.15, *)
2020
public 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

Comments
 (0)