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

mlange-42 / arche-model / 9209952348

23 May 2024 02:34PM CUT coverage: 98.452% (+0.06%) from 98.395%
9209952348

push

github

web-flow
Add `reporter.TableCallback` for direct retrieval of table observer output in Go code (#66)

26 of 26 new or added lines in 1 file covered. (100.0%)

636 of 646 relevant lines covered (98.45%)

139.8 hits per line

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

100.0
/model/systems.go
1
package model
2

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

7
        "github.com/mlange-42/arche-model/resource"
8
        "github.com/mlange-42/arche/ecs"
9
        "github.com/mlange-42/arche/generic"
10
)
11

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

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

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

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

56
        nextDraw   time.Time
57
        nextUpdate time.Time
58

59
        initialized bool
60
        locked      bool
61

62
        tickRes generic.Resource[resource.Tick]
63
        termRes generic.Resource[resource.Termination]
64
}
65

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

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

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

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

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

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

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

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

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

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

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

183
// Initialize all systems.
184
func (s *Systems) initialize() {
30✔
185
        if s.initialized {
33✔
186
                panic("model is already initialized")
3✔
187
        }
188

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

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

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

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

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

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

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

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

229
        return !s.termRes.Get().Terminate
1,402✔
230
}
231

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

130✔
244
        s.removeSystems()
130✔
245

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

364
        s.finalize()
23✔
365
}
366

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

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

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

381
// Calculates frame rate capped to target
382
func (s *Systems) limitedFps(actual, target float64) float64 {
136✔
383
        if actual > target || actual <= 0 {
171✔
384
                return target
35✔
385
        }
35✔
386
        return actual
101✔
387
}
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