Skip to content

Commit 6a46e40

Browse files
committed
Improve KeyboardControlledThrustComponent
- Adopt AcceleratedValue - Add TimeStep options - Improve documentation and comments
1 parent 77735f7 commit 6a46e40

File tree

1 file changed

+63
-34
lines changed

1 file changed

+63
-34
lines changed

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

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import GameplayKit
1919
@available(macOS 10.15, *)
2020
public final class KeyboardControlledThrustComponent: OctopusComponent, OctopusUpdatableComponent {
2121

22+
// TODO: Tests
23+
24+
// DESIGN: A `resetAccelerationWhenChangingDirection` is probably not needed because the inertia and friction of the physics body should take care of that anyway, right?
25+
2226
public override var requiredComponents: [GKComponent.Type]? {
2327
[KeyboardEventComponent.self,
2428
PhysicsComponent.self,
@@ -30,77 +34,102 @@ public final class KeyboardControlledThrustComponent: OctopusComponent, OctopusU
3034
/// Change this to a different code to customize the keys.
3135
public var arrowDown: UInt16 = .arrowDown
3236

33-
public var baseMagnitudePerSecond: CGFloat
34-
public var maximumMagnitudePerSecond: CGFloat
35-
public var acceleratedMagnitude: CGFloat = 0
36-
37-
/// The amount to increase `acceleratedMagnitude` by per second, while there is keyboard input. `acceleratedMagnitude` is reset to `baseMagnitudePerSecond` when there is no keyboard input.
38-
public var accelerationPerSecond: CGFloat
37+
/// The amount of thrust to apply in a single update, with optional acceleration. Affected by `timestep`. Reset when there is no keyboard input.
38+
public var magnitudePerUpdate: AcceleratedValue<CGFloat>
3939

4040
/// Multiplies the force by the specified value. Default: `1`. To reverse the thrust, specify a negative value like `-1`. To disable thrust, specify `0`.
41-
public var factor: CGFloat = 1
41+
public var scalingFactor: CGFloat = 1
42+
43+
/// Specifies a fixed or variable timestep for per-update changes.
44+
public var timestep: TimeStep
4245

4346
/// - Parameters:
44-
/// - baseMagnitudePerSecond: The minimum magnitude to apply to the physics body every second.
45-
/// - maximumMagnitudePerSecond: The maximum magnitude to allow after acceleration has been applied.
46-
/// - accelerationPerSecond: The amount to increase the magnitude by per second, while there is keyboard input. The magnitude is reset to the `baseMagnitudePerSecond` when there is no keyboard input.
47-
/// - factor: Multiply the force by this factor. Default: `1`. To reverse the thrust, specify a negative value like `-1`. To disable thrust, specify `0`.
48-
public init(baseMagnitudePerSecond: CGFloat = 600, // ÷ 60 = 10 per frame
49-
maximumMagnitudePerSecond: CGFloat = 1200, // ÷ 60 = 20 per frame
50-
accelerationPerSecond: CGFloat = 600,
51-
factor: CGFloat = 1)
47+
/// - magnitudePerUpdate: The amount of thrust to apply every update, with optional acceleration. Affected by `timestep`.
48+
/// - scalingFactor: Multiplies the force by the specified factor. Default: `1`. To reverse the thrust, specify a negative value like `-1`. To disable thrust, specify `0`.
49+
/// - timestep: Specifies a fixed or variable timestep for per-update changes. Default: `.perSecond`
50+
public init(magnitudePerUpdate: AcceleratedValue<CGFloat>,
51+
scalingFactor: CGFloat = 1.0,
52+
timestep: TimeStep = .perSecond)
5253
{
53-
self.baseMagnitudePerSecond = baseMagnitudePerSecond
54-
self.maximumMagnitudePerSecond = maximumMagnitudePerSecond
55-
self.accelerationPerSecond = accelerationPerSecond
56-
self.factor = factor
54+
self.magnitudePerUpdate = magnitudePerUpdate
55+
self.scalingFactor = scalingFactor
56+
self.timestep = timestep
5757
super.init()
5858
}
5959

60+
/// - Parameters:
61+
/// - magnitudePerUpdate: The minimum magnitude to apply to the physics body every second. Affected by `timestep`.
62+
/// - acceleration: The amount to increase the magnitude by per second, while there is keyboard input. The magnitude is reset to the `baseMagnitudePerSecond` when there is no keyboard input. Affected by `timestep`.
63+
/// - maximum: The maximum magnitude to allow after acceleration has been applied.
64+
/// - scalingFactor: Multiplies the force by the specified factor. Default: `1`. To reverse the thrust, specify a negative value like `-1`. To disable thrust, specify `0`.
65+
/// - timestep: Specifies a fixed or variable timestep for per-update changes. Default: `.perSecond`
66+
public convenience init(magnitudePerUpdate: CGFloat = 600, // ÷ 60 = 10 per frame
67+
acceleration: CGFloat = 100,
68+
maximum: CGFloat = 900, // ÷ 60 = 15 per frame
69+
scalingFactor: CGFloat = 1,
70+
timestep: TimeStep = .perSecond)
71+
{
72+
self.init(magnitudePerUpdate: AcceleratedValue<CGFloat>(base: magnitudePerUpdate,
73+
current: magnitudePerUpdate,
74+
maximum: maximum,
75+
minimum: 0,
76+
acceleration: acceleration),
77+
scalingFactor: scalingFactor,
78+
timestep: timestep)
79+
}
80+
6081
public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
6182

6283
@inlinable
6384
public override func update(deltaTime seconds: TimeInterval) {
6485

86+
// #0: If there is no input or valid entity for this frame, reset the acceleration and exit.
87+
6588
guard
6689
let keyboardEventComponent = coComponent(KeyboardEventComponent.self),
6790
!keyboardEventComponent.codesPressed.isEmpty,
6891
let node = entityNode,
6992
let physicsBody = coComponent(PhysicsComponent.self)?.physicsBody ?? node.physicsBody
7093
else {
71-
acceleratedMagnitude = baseMagnitudePerSecond // TODO: PERFORMANCE: Figure out a better way than setting this every frame.
94+
magnitudePerUpdate.reset() // TODO: PERFORMANCE: Figure out a better way than setting this every frame.
7295
return
7396
}
7497

75-
// Did player press a directional arrow key?
98+
// #1: Did player press a directional arrow key?
99+
76100
// ❕ NOTE: Don't use `switch` or `else` because we want to process multiple keypresses, to cancel out opposing directions.
77101

78102
let codesPressed = keyboardEventComponent.codesPressed
79103
var direction: CGFloat = 0
80104

81-
if codesPressed.contains(self.arrowUp) { direction += 1 } // ⬆️
82-
if codesPressed.contains(self.arrowDown) { direction -= 1 } // ⬇️
105+
if codesPressed.contains(self.arrowUp) { direction += 1 } // ⬆️
106+
if codesPressed.contains(self.arrowDown) { direction -= 1 } // ⬇️
107+
108+
// #2: Exit if multiple directional inputs cancel each other out, this prevents accumulation of acceleration when there is no movement.
109+
110+
guard direction != 0 else {
111+
magnitudePerUpdate.reset()
112+
return
113+
}
83114

84-
// Apply the force in relation to the node's current rotation.
115+
// #3: Apply the force in relation to the node's current rotation.
85116

86-
var magnitudeForCurrentFrame = acceleratedMagnitude * CGFloat(seconds)
87-
let vector = CGVector(radians: node.zRotation) * CGFloat(magnitudeForCurrentFrame * direction) * factor // TODO: Verify!
117+
var magnitudeForCurrentFrame = timestep.applying(magnitudePerUpdate.current, deltaTime: CGFloat(seconds))
118+
let vector = CGVector(radians: node.zRotation) * CGFloat(magnitudeForCurrentFrame * direction) * scalingFactor // TODO: Verify!
88119

89120
// Apply the final vector to the body.
90121

91122
#if LOGINPUTEVENTS
92-
debugLog("acceleratedMagnitude: \(acceleratedMagnitude), magnitudeForCurrentFrame: \(magnitudeForCurrentFrame), factor: \(factor), rotation: \(node.zRotation), force: \(vector)")
123+
debugLog("magnitudePerUpdate: \(magnitudePerUpdate), magnitudeForCurrentFrame: \(magnitudeForCurrentFrame), scalingFactor: \(scalingFactor), rotation: \(node.zRotation), force: \(vector)")
93124
#endif
94125

95126
physicsBody.applyForce(vector)
96127

97-
// Apply acceleration for the next frame.
128+
// #4: Apply acceleration for the next frame.
98129

99-
if acceleratedMagnitude < maximumMagnitudePerSecond {
100-
acceleratedMagnitude += (accelerationPerSecond * CGFloat(seconds))
101-
if acceleratedMagnitude > maximumMagnitudePerSecond {
102-
acceleratedMagnitude = maximumMagnitudePerSecond
103-
}
130+
if magnitudePerUpdate.isWithinBounds { // CHECK: PERFORMANCE
131+
magnitudePerUpdate.update(timestep: timestep, deltaTime: CGFloat(seconds))
132+
magnitudePerUpdate.clamp()
104133
}
105134
}
106135
}
@@ -109,7 +138,7 @@ public final class KeyboardControlledThrustComponent: OctopusComponent, OctopusU
109138

110139
#if !canImport(AppKit)
111140
// TODO: Add support for iOS/tvOS keyboards.
112-
@available(iOS, unavailable)
141+
@available(iOS, unavailable)
113142
@available(tvOS, unavailable)
114143
public final class KeyboardControlledThrustComponent: macOSExclusiveComponent {}
115144
#endif

0 commit comments

Comments
 (0)