• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mlange-42 / ark-tools / 13609263776

01 Mar 2025 10:08PM CUT coverage: 100.0%. First build
13609263776

Pull #1

github

web-flow
Merge 737eee7f4 into 3e01f444a
Pull Request #1: Add App and required resources

343 of 343 new or added lines in 7 files covered. (100.0%)

343 of 343 relevant lines covered (100.0%)

219.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/app/systems.go
1
package app
2

3
import (
4
        "fmt"
5
        "time"
6

7
        "github.com/mlange-42/ark-tools/resource"
8
        "github.com/mlange-42/ark/ecs"
9
)
10

11
// System is the interface for ECS systems.
12
//
13
// See also [UISystem] for systems with an independent graphics step.
14
type System interface {
15
        Initialize(w *ecs.World) // Initialize the system.
16
        Update(w *ecs.World)     // Update the system.
17
        Finalize(w *ecs.World)   // Finalize the system.
18
}
19

20
// UISystem is the interface for ECS systems that display UI in an independent graphics step.
21
//
22
// See also [System] for normal systems.
23
type UISystem interface {
24
        InitializeUI(w *ecs.World) // InitializeUI the system.
25
        UpdateUI(w *ecs.World)     // UpdateUI/update the system.
26
        PostUpdateUI(w *ecs.World) // PostUpdateUI does the final part of updating, e.g. update the GL window.
27
        FinalizeUI(w *ecs.World)   // FinalizeUI the system.
28
}
29

30
// Systems manages and schedules ECS [System] and [UISystem] instances.
31
//
32
// [System] instances are updated with a frequency given by TPS (ticks per second).
33
// [UISystem] instances are updated independently of normal systems, with a frequency given by FPS (frames per second).
34
//
35
// [Systems] is an embed in [App] and it's methods are usually only used through a [App] instance.
36
// By also being a resource of each [App], however, systems can access it and e.g. remove themselves from aan app.
37
type Systems struct {
38
        // Ticks per second for normal systems.
39
        // Values <= 0 (the default) mean as fast as possible.
40
        TPS float64
41
        // Frames per second for UI systems.
42
        // A zero/unset value defaults to 30 FPS. Values < 0 sync FPS with TPS.
43
        // With fast movement, a value of 60 may be required for fluent graphics.
44
        FPS float64
45
        // Whether the simulation is currently paused.
46
        // When paused, only UI updates but no normal updates are performed.
47
        Paused bool
48

49
        world      *ecs.World
50
        systems    []System
51
        uiSystems  []UISystem
52
        toRemove   []System
53
        uiToRemove []UISystem
54

55
        nextDraw   time.Time
56
        nextUpdate time.Time
57

58
        initialized bool
59
        locked      bool
60

61
        tickRes ecs.Resource[resource.Tick]
62
        termRes ecs.Resource[resource.Termination]
63
}
64

65
// Systems returns the normal/non-UI systems.
66
func (s *Systems) Systems() []System {
3✔
67
        return s.systems
3✔
68
}
3✔
69

70
// UISystems returns the UI systems.
71
func (s *Systems) UISystems() []UISystem {
3✔
72
        return s.uiSystems
3✔
73
}
3✔
74

75
// AddSystem adds a [System] to the app.
76
//
77
// Panics if the system is also a [UISystem].
78
// To add systems that implement both [System] and [UISystem], use [Systems.AddUISystem]
79
func (s *Systems) AddSystem(sys System) {
38✔
80
        if s.initialized {
41✔
81
                panic("adding systems after app initialization is not implemented yet")
3✔
82
        }
83
        if sys, ok := sys.(UISystem); ok {
38✔
84
                panic(fmt.Sprintf("System %T is also an UI system. Must be added via AddUISystem.", sys))
3✔
85
        }
86
        s.systems = append(s.systems, sys)
32✔
87
}
88

89
// AddUISystem adds an [UISystem] to the app.
90
//
91
// Adds the [UISystem] also as a normal [System] if it implements the interface.
92
func (s *Systems) AddUISystem(sys UISystem) {
14✔
93
        if s.initialized {
17✔
94
                panic("adding systems after app initialization is not implemented yet")
3✔
95
        }
96
        s.uiSystems = append(s.uiSystems, sys)
11✔
97
        if sys, ok := sys.(System); ok {
14✔
98
                s.systems = append(s.systems, sys)
3✔
99
        }
3✔
100
}
101

102
// RemoveSystem removes a system from the app.
103
//
104
// Systems can also be removed during a run.
105
// However, this will take effect only after the end of the full update step.
106
func (s *Systems) RemoveSystem(sys System) {
9✔
107
        if sys, ok := sys.(UISystem); ok {
12✔
108
                panic(fmt.Sprintf("System %T is also an UI system. Must be removed via RemoveUISystem.", sys))
3✔
109
        }
110
        s.toRemove = append(s.toRemove, sys)
6✔
111
        if !s.locked {
9✔
112
                s.removeSystems()
3✔
113
        }
3✔
114
}
115

116
// RemoveUISystem removes an UI system from the app.
117
//
118
// Systems can also be removed during a run.
119
// However, this will take effect only after the end of the full update step.
120
func (s *Systems) RemoveUISystem(sys UISystem) {
12✔
121
        s.uiToRemove = append(s.uiToRemove, sys)
12✔
122
        if !s.locked {
21✔
123
                s.removeSystems()
9✔
124
        }
9✔
125
}
126

127
// Removes systems that were removed during the update step.
128
func (s *Systems) removeSystems() {
1,622✔
129
        rem := s.toRemove
1,622✔
130
        remUI := s.uiToRemove
1,622✔
131

1,622✔
132
        s.toRemove = s.toRemove[:0]
1,622✔
133
        s.uiToRemove = s.uiToRemove[:0]
1,622✔
134

1,622✔
135
        for _, sys := range rem {
1,628✔
136
                s.removeSystem(sys)
6✔
137
        }
6✔
138
        for _, sys := range remUI {
1,631✔
139
                if sys, ok := sys.(System); ok {
18✔
140
                        s.removeSystem(sys)
6✔
141
                }
6✔
142
                s.removeUISystem(sys)
9✔
143
        }
144
}
145

146
func (s *Systems) removeSystem(sys System) {
15✔
147
        if s.locked {
18✔
148
                panic("can't remove a system in locked state")
3✔
149
        }
150
        idx := -1
12✔
151
        for i := 0; i < len(s.systems); i++ {
33✔
152
                if sys == s.systems[i] {
27✔
153
                        idx = i
6✔
154
                        break
6✔
155
                }
156
        }
157
        if idx < 0 {
18✔
158
                panic(fmt.Sprintf("can't remove system %T: not in the app", sys))
6✔
159
        }
160
        s.systems[idx].Finalize(s.world)
6✔
161
        s.systems = append(s.systems[:idx], s.systems[idx+1:]...)
6✔
162
}
163

164
func (s *Systems) removeUISystem(sys UISystem) {
12✔
165
        if s.locked {
15✔
166
                panic("can't remove a system in locked state")
3✔
167
        }
168
        idx := -1
9✔
169
        for i := 0; i < len(s.uiSystems); i++ {
15✔
170
                if sys == s.uiSystems[i] {
12✔
171
                        idx = i
6✔
172
                        break
6✔
173
                }
174
        }
175
        if idx < 0 {
12✔
176
                panic(fmt.Sprintf("can't remove UI system %T: not in the app", sys))
3✔
177
        }
178
        s.uiSystems[idx].FinalizeUI(s.world)
6✔
179
        s.uiSystems = append(s.uiSystems[:idx], s.uiSystems[idx+1:]...)
6✔
180
}
181

182
// Initialize all systems.
183
func (s *Systems) initialize() {
29✔
184
        if s.initialized {
32✔
185
                panic("app is already initialized")
3✔
186
        }
187

188
        if s.FPS == 0 {
28✔
189
                s.FPS = 30
2✔
190
        }
2✔
191

192
        s.tickRes = ecs.NewResource[resource.Tick](s.world)
26✔
193
        s.termRes = ecs.NewResource[resource.Termination](s.world)
26✔
194

26✔
195
        s.locked = true
26✔
196
        for _, sys := range s.systems {
61✔
197
                sys.Initialize(s.world)
35✔
198
        }
35✔
199
        for _, sys := range s.uiSystems {
37✔
200
                sys.InitializeUI(s.world)
11✔
201
        }
11✔
202
        s.locked = false
26✔
203
        s.removeSystems()
26✔
204
        s.initialized = true
26✔
205

26✔
206
        s.nextDraw = time.Time{}
26✔
207
        s.nextUpdate = time.Time{}
26✔
208

26✔
209
        s.tickRes.Get().Tick = 0
26✔
210
}
211

212
// Update all systems.
213
func (s *Systems) update() bool {
1,302✔
214
        s.locked = true
1,302✔
215
        update := s.updateSystemsTimed()
1,302✔
216
        s.updateUISystemsTimed(update)
1,302✔
217
        s.locked = false
1,302✔
218

1,302✔
219
        s.removeSystems()
1,302✔
220

1,302✔
221
        if update {
2,482✔
222
                time := s.tickRes.Get()
1,180✔
223
                time.Tick++
1,180✔
224
        } else {
1,302✔
225
                s.wait()
122✔
226
        }
122✔
227

228
        return !s.termRes.Get().Terminate
1,302✔
229
}
230

231
// updateSystems updates all normal systems
232
func (s *Systems) updateSystems() bool {
136✔
233
        if !s.initialized {
139✔
234
                panic("the app is not initialized")
3✔
235
        }
236
        if s.Paused {
136✔
237
                return true
3✔
238
        }
3✔
239
        s.locked = true
130✔
240
        updated := s.updateSystemsSimple()
130✔
241
        s.locked = false
130✔
242

130✔
243
        s.removeSystems()
130✔
244

130✔
245
        if updated {
260✔
246
                time := s.tickRes.Get()
130✔
247
                time.Tick++
130✔
248
        }
130✔
249

250
        return !s.termRes.Get().Terminate
130✔
251
}
252

253
// updateUISystems updates all UI systems
254
func (s *Systems) updateUISystems() {
129✔
255
        if !s.initialized {
132✔
256
                panic("the app is not initialized")
3✔
257
        }
258
        s.locked = true
126✔
259
        s.updateUISystemsSimple()
126✔
260
        s.locked = false
126✔
261

126✔
262
        s.removeSystems()
126✔
263
}
264

265
// Calculates and waits the time until the next update of UI update.
266
func (s *Systems) wait() {
122✔
267
        nextUpdate := s.nextUpdate
122✔
268

122✔
269
        if (s.Paused || s.FPS > 0) && s.nextDraw.Before(nextUpdate) {
201✔
270
                nextUpdate = s.nextDraw
79✔
271
        }
79✔
272

273
        t := time.Now()
122✔
274
        wait := nextUpdate.Sub(t)
122✔
275

122✔
276
        if wait > 0 {
243✔
277
                time.Sleep(wait)
121✔
278
        }
121✔
279
}
280

281
// Update normal systems.
282
func (s *Systems) updateSystemsSimple() bool {
1,310✔
283
        for _, sys := range s.systems {
2,692✔
284
                sys.Update(s.world)
1,382✔
285
        }
1,382✔
286
        return true
1,310✔
287
}
288

289
// Update normal systems.
290
func (s *Systems) updateSystemsTimed() bool {
1,302✔
291
        update := false
1,302✔
292
        if s.Paused {
1,403✔
293
                update = !time.Now().Before(s.nextUpdate)
101✔
294
                if update {
136✔
295
                        tps := s.limitedFps(s.TPS, 10)
35✔
296
                        s.nextUpdate = nextTime(s.nextUpdate, tps)
35✔
297
                }
35✔
298
                return false
101✔
299
        }
300
        if s.TPS <= 0 {
2,366✔
301
                update = true
1,165✔
302
                s.updateSystemsSimple()
1,165✔
303
        } else {
1,201✔
304
                update = !time.Now().Before(s.nextUpdate)
36✔
305
                if update {
51✔
306
                        s.nextUpdate = nextTime(s.nextUpdate, s.TPS)
15✔
307
                        s.updateSystemsSimple()
15✔
308
                }
15✔
309
        }
310
        return update
1,201✔
311
}
312

313
// Update ui systems.
314
func (s *Systems) updateUISystemsSimple() {
290✔
315
        for _, sys := range s.uiSystems {
432✔
316
                sys.UpdateUI(s.world)
142✔
317
        }
142✔
318
        for _, sys := range s.uiSystems {
432✔
319
                sys.PostUpdateUI(s.world)
142✔
320
        }
142✔
321
}
322

323
// Update UI systems.
324
func (s *Systems) updateUISystemsTimed(updated bool) {
1,302✔
325
        if !s.Paused && s.FPS <= 0 {
1,310✔
326
                if updated {
13✔
327
                        s.updateUISystemsSimple()
5✔
328
                }
5✔
329
        } else {
1,294✔
330
                if !time.Now().Before(s.nextDraw) {
1,453✔
331
                        fps := s.FPS
159✔
332
                        if s.Paused {
260✔
333
                                fps = s.limitedFps(s.FPS, 30)
101✔
334
                        }
101✔
335
                        s.nextDraw = nextTime(s.nextDraw, fps)
159✔
336
                        s.updateUISystemsSimple()
159✔
337
                }
338
        }
339
}
340

341
// Finalize all systems.
342
func (s *Systems) finalize() {
26✔
343
        s.locked = true
26✔
344
        for _, sys := range s.systems {
58✔
345
                sys.Finalize(s.world)
32✔
346
        }
32✔
347
        for _, sys := range s.uiSystems {
34✔
348
                sys.FinalizeUI(s.world)
8✔
349
        }
8✔
350
        s.locked = false
26✔
351
        s.removeSystems()
26✔
352
}
353

354
// Run the app.
355
func (s *Systems) run() {
22✔
356
        if !s.initialized {
44✔
357
                s.initialize()
22✔
358
        }
22✔
359

360
        for s.update() {
1,302✔
361
        }
1,280✔
362

363
        s.finalize()
22✔
364
}
365

366
// Removes all systems.
367
func (s *Systems) reset() {
19✔
368
        s.systems = []System{}
19✔
369
        s.uiSystems = []UISystem{}
19✔
370
        s.toRemove = s.toRemove[:0]
19✔
371
        s.uiToRemove = s.uiToRemove[:0]
19✔
372

19✔
373
        s.nextDraw = time.Time{}
19✔
374
        s.nextUpdate = time.Time{}
19✔
375

19✔
376
        s.initialized = false
19✔
377
        s.tickRes = ecs.Resource[resource.Tick]{}
19✔
378
}
19✔
379

380
// Calculates frame rate capped to target
381
func (s *Systems) limitedFps(actual, target float64) float64 {
136✔
382
        if actual > target || actual <= 0 {
171✔
383
                return target
35✔
384
        }
35✔
385
        return actual
101✔
386
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc