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

mlange-42 / ark-tools / 13944569950

19 Mar 2025 10:41AM CUT coverage: 98.445%. Remained the same
13944569950

push

github

web-flow
Upgrade to Ark v0.4.0 (#9)

633 of 643 relevant lines covered (98.44%)

140.44 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) {
40✔
80
        if s.initialized {
43✔
81
                panic("adding systems after app initialization is not implemented yet")
3✔
82
        }
83
        if sys, ok := sys.(UISystem); ok {
40✔
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)
34✔
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) {
10✔
107
        if sys, ok := sys.(UISystem); ok {
13✔
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)
7✔
111
        if !s.locked {
11✔
112
                s.removeSystems()
4✔
113
        }
4✔
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,725✔
129
        rem := s.toRemove
1,725✔
130
        remUI := s.uiToRemove
1,725✔
131

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

1,725✔
135
        for _, sys := range rem {
1,732✔
136
                s.removeSystem(sys)
7✔
137
        }
7✔
138
        for _, sys := range remUI {
1,734✔
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) {
16✔
147
        if s.locked {
19✔
148
                panic("can't remove a system in locked state")
3✔
149
        }
150
        idx := -1
13✔
151
        for i := 0; i < len(s.systems); i++ {
35✔
152
                if sys == s.systems[i] {
29✔
153
                        idx = i
7✔
154
                        break
7✔
155
                }
156
        }
157
        if idx < 0 {
19✔
158
                panic(fmt.Sprintf("can't remove system %T: not in the app", sys))
6✔
159
        }
160
        s.systems[idx].Finalize(s.world)
7✔
161
        s.systems = append(s.systems[:idx], s.systems[idx+1:]...)
7✔
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() {
30✔
184
        if s.initialized {
33✔
185
                panic("app is already initialized")
3✔
186
        }
187

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

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

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

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

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

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

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

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

228
        return !s.termRes.Get().Terminate
1,402✔
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,410✔
283
        for _, sys := range s.systems {
2,892✔
284
                sys.Update(s.world)
1,482✔
285
        }
1,482✔
286
        return true
1,410✔
287
}
288

289
// Update normal systems.
290
func (s *Systems) updateSystemsTimed() bool {
1,402✔
291
        update := false
1,402✔
292
        if s.Paused {
1,503✔
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,566✔
301
                update = true
1,265✔
302
                s.updateSystemsSimple()
1,265✔
303
        } else {
1,301✔
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,301✔
311
}
312

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

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

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

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

360
        for s.update() {
1,402✔
361
        }
1,379✔
362

363
        s.finalize()
23✔
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