-
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathUserInput.swift
224 lines (181 loc) · 12.3 KB
/
UserInput.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
/*
* Copyright © 2023-2024 Dustin Collins (Strega's Gate)
* All Rights Reserved.
*
* http://stregasgate.com
*/
import GateEngine
@main
final class UserInputGameDelegate: GameDelegate {
// didFinishLaunching() is executed immediatley after the game is ready to start
func didFinishLaunching(game: Game, options: LaunchOptions) {
// Add our projects sytem which is implemented below
game.insertSystem(UserInputSystem.self)
// Add the projects rendering system to the game which implementation is below
game.insertSystem(TextRenderingSystem.self)
// Set the main window's title
game.windowManager.mainWindow?.title = "User Input"
}
}
// We need a component to store some text to draw
// Creating Text is an async process, and it wont necessarily be renderable instantly
// We must store it in a component so the Text has a chance to load.
// Passing it directly to Canvas will likely cause it to deallocate before it is ever shown on screen
final class TextComponent: Component {
// Text is a renderable object that hold a string and associated meta like font, pointSize, and color
@MainActor
let text = Text(string: "Press Something...", pointSize: 64, color: .white)
// All components require a componentID. For performance reasons make it a let.
// ComponentID has single init() so we'll always just create a new one with no options.
static let componentID: ComponentID = ComponentID()
}
// System subclasses are used to manipulate the simulation. They can't be used to draw content.
class UserInputSystem: System {
// This value stores Input states and is used to check when an input has changed
var inputReceipts = InputReceipts()
// setup() is executed a single time when the System is added to the game
override func setup(game: Game, input: HID) async {
// Create an entity
let entity = Entity()
// Give the entity the TextComponent we made
entity.insert(TextComponent.self)
// Add the entity to the game
game.insertEntity(entity)
}
// update() is executed every simulation tick, which may or may not be every frame
override func update(game: Game, input: HID, withTimePassed deltaTime: Float) async {
// Get the entity from the game
if let entity = game.firstEntity(withComponent: TextComponent.self) {
// Unwrap the TextComponent
entity.modify(TextComponent.self) { [self] component in
// MARK: - Gamepads
// The "any" gamepad will return the most recently used gamepad.
// Use this for single user games so the user can swap controllers seamlessly.
// isPressed(ifDifferent:) will return true if the button is currently down but was previously up
input.gamePads.any.button.north.whenPressed(ifDifferent: &inputReceipts) { button in
// the symbol property will return the platform button for the physical gamepad
// For example on DualShock south is .cross, and on Xbox south is .A
// Symbols for console gamepads are guaranteed, otherwise GateEngine will guess
// based on the host OS and the physical gampad's manufacturer name
component.text.string += "\n\(button.symbol) pressed!"
}
input.gamePads.any.button.south.whenPressed(ifDifferent: &inputReceipts) { button in
component.text.string += "\n\(button.symbol) pressed!"
}
input.gamePads.any.button.east.whenPressed(ifDifferent: &inputReceipts) { button in
component.text.string += "\n\(button.symbol) pressed!"
}
input.gamePads.any.button.west.whenPressed(ifDifferent: &inputReceipts) { button in
component.text.string += "\n\(button.symbol) pressed!"
}
if input.gamePads.any.stick.right.isPushed {
component.text.string += String(format: "\nR-Stick Pushed x: %.3f, y: %.3f", input.gamePads.any.stick.right.xAxis, input.gamePads.any.stick.right.yAxis)
}
if input.gamePads.any.stick.left.isPushed {
component.text.string += String(format: "\nL-Stick Pushed x: %.3f, y: %.3f", input.gamePads.any.stick.left.xAxis, input.gamePads.any.stick.left.yAxis)
}
if input.gamePads.any.trigger.left.isPressed {
component.text.string += String(format: "\nL-Trigger Pressed %.3f", input.gamePads.any.trigger.left.value)
}
if input.gamePads.any.trigger.right.isPressed {
component.text.string += String(format: "\nR-Trigger Pressed %.3f", input.gamePads.any.trigger.right.value)
}
// MARK: - Keyboard
// Keyboard checks work similar to gamepad buttons, see above
if input.keyboard.button(.azerty("z")).isPressed(ifDifferent: &inputReceipts) {
component.text.string += "\nW pressed!"
}else if input.keyboard.button("s").isPressed(ifDifferent: &inputReceipts) {
component.text.string += "\nS pressed!"
}else if input.keyboard.button("a").isPressed(ifDifferent: &inputReceipts) {
component.text.string += "\nA pressed!"
}else if input.keyboard.button("d").isPressed(ifDifferent: &inputReceipts) {
component.text.string += "\nD pressed!"
}else if let button = input.keyboard.pressedButtons().first?.button {
button.whenPressed(ifDifferent: &inputReceipts) { button in
component.text.string += "\n\(button) pressed!"
}
}
// MARK: - Mouse
// Mouse buttons work similar to gamepad buttons, see above
input.mouse.button(.button1).whenPressed(ifDifferent: &inputReceipts) { button in
component.text.string += String(format: "\nPrimary Click (%i) at x: %.0f, y: %.0f", button.pressCount,
input.mouse.position!.x,
input.mouse.position!.y)
}
input.mouse.button(.button2).whenPressed(ifDifferent: &inputReceipts) { button in
component.text.string += String(format: "\nSecondary Click (%i) at x: %.0f, y: %.0f", button.pressCount,
input.mouse.position!.x,
input.mouse.position!.y)
}
input.mouse.button(.button3).whenPressed(ifDifferent: &inputReceipts) { button in
component.text.string += String(format: "\nMiddle Click (%i) at x: %.0f, y: %.0f", button.pressCount,
input.mouse.position!.x,
input.mouse.position!.y)
}
input.mouse.button(.button4).whenPressed(ifDifferent: &inputReceipts) { button in
component.text.string += String(format: "\nButton 4 Click (%i) at x: %.0f, y: %.0f", button.pressCount,
input.mouse.position!.x,
input.mouse.position!.y)
}
input.mouse.button(.button5).whenPressed(ifDifferent: &inputReceipts) { button in
component.text.string += String(format: "\nButton 5 Click (%i) at x: %.0f, y: %.0f", button.pressCount,
input.mouse.position!.x,
input.mouse.position!.y)
}
input.mouse.scroller(.horizontal).whenScrolled(ifDifferent: &inputReceipts) { scroller in
let direction = (scroller.direction == .positive) ? "Right" : "Left"
component.text.string += String(format: "\nScrolled %i (%@) Delta: %.3f, UI Delta: %.3f", scroller.ticks, direction, scroller.delta, scroller.uiDelta)
}
input.mouse.scroller(.vertical).whenScrolled(ifDifferent: &inputReceipts) { scroller in
let direction = (scroller.direction == .positive) ? "Up" : "Down"
component.text.string += String(format: "\nScrolled %i (%@) Delta: %.3f, UI Delta: %.3f", scroller.ticks, direction, scroller.delta, scroller.uiDelta)
}
// MARK: - Touch
// Touches can be on screen, indirect such as a trackpad or gamepad. Use the Touch.kind property to ensure you treat the touch appropriatley
// as the user will expect certain things like screens to be pixel perfect and trackpads to be relative.
// A Touch.phase is valid for this update only. The .up phase is available exactly once before the Touch is removed, so be sure to check the phase every frame.
if let touch = input.screen.touches.first(where: {$0.phase == .up}) {
component.text.string += String(format: "\nScreen Touch Up at x: %.3f, y: %.3f", touch.position.x, touch.position.y)
}
if let touch = input.surfaces.any?.touches.first(where: {$0.phase == .up}) {
component.text.string += String(format: "\nSurface Touch Up at x: %.3f, y: %.3f", touch.position.x, touch.position.y)
}
var components = component.text.string.components(separatedBy: "\n")
while components.count > 5 {
components.remove(at: 0)
}
component.text.string = components.joined(separator: "\n")
}
}
}
// phase determines at which point the system should be updated relative to other systems
override class var phase: System.Phase {.simulation}
}
// RenderingSystem subclasses can draw content
// However, updating the simulation from a RenderingSystem is a programming error
// GateEngine allows for frame drops and headless execution for servers
// In these cases RenderingSystems do not get updated
class TextRenderingSystem: RenderingSystem {
// render() is called only when drawing needs to be done
override func render(game: Game, window: Window, withTimePassed deltaTime: Float) {
// A Canvas is a drawing container for 2D objects
// Canvas is light weight and you're meant to create a new one every frame
var canvas = Canvas()
// Loop through all entites in the game
for entity in game.entities {
// Make sure the entity has a TextComponent, otherwise move on
guard let text = entity.component(ofType: TextComponent.self)?.text else {continue}
// Create a Rect the size of the window and get it's center
let windowCenter = Rect(size: window.size).center
// Half of the rendered text size
let halfTextSize = text.size / 2
// Subtract half the text size from the window center
// to get a position that will center the text
let position = windowCenter - halfTextSize
// Add the text to the canvas at our centerd position
canvas.insert(text, at: position)
}
// Add the canvas to the framebuffer to be drawn
window.insert(canvas)
}
}