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

mlange-42 / arche-model / 4671051189

11 Apr 2023 07:02PM CUT coverage: 95.467%. First build
4671051189

push

github

GitHub
More tests, coverage via coveralls.io (#30)

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

358 of 375 relevant lines covered (95.47%)

140.63 hits per line

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

96.28
/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
        TPS    float64 // Ticks per second for normal systems. Values <= 0 (the default) mean as fast as possible.
40
        FPS    float64 // Frames per second for UI systems. A zero/unset value defaults to 30 FPS. Values < 0 sync FPS with TPS.
41
        Paused bool    // Whether the simulation is currently paused. When paused, only UI updates but no normal updates are performed.
42

43
        world      *ecs.World
44
        systems    []System
45
        uiSystems  []UISystem
46
        toRemove   []System
47
        uiToRemove []UISystem
48

49
        nextDraw   time.Time
50
        nextUpdate time.Time
51

52
        initialized bool
53
        locked      bool
54

55
        tickRes generic.Resource[resource.Tick]
56
        termRes generic.Resource[resource.Termination]
57
}
58

59
// AddSystem adds a [System] to the model.
60
//
61
// Panics if the system is also a [UISystem].
62
// To add systems that implement both [System] and [UISystem], use [Systems.AddUISystem]
63
func (s *Systems) AddSystem(sys System) {
35✔
64
        if s.initialized {
38✔
65
                panic("adding systems after model initialization is not implemented yet")
3✔
66
        }
67
        if sys, ok := sys.(UISystem); ok {
35✔
68
                panic(fmt.Sprintf("System %T is also an UI system. Must be added via AddSystem.", sys))
3✔
69
        }
70
        s.systems = append(s.systems, sys)
29✔
71
}
72

73
// AddUISystem adds an [UISystem] to the model.
74
//
75
// Adds the [UISystem] also as a normal [System] if it implements the interface.
76
func (s *Systems) AddUISystem(sys UISystem) {
13✔
77
        if s.initialized {
16✔
78
                panic("adding systems after model initialization is not implemented yet")
3✔
79
        }
80
        s.uiSystems = append(s.uiSystems, sys)
10✔
81
        if sys, ok := sys.(System); ok {
13✔
82
                s.systems = append(s.systems, sys)
3✔
83
        }
3✔
84
}
85

86
// RemoveSystem removes a system from the model.
87
//
88
// Systems can also be removed during a model run.
89
// However, this will take effect only after the end of the full model step.
90
func (s *Systems) RemoveSystem(sys System) {
10✔
91
        if sys, ok := sys.(UISystem); ok {
13✔
92
                panic(fmt.Sprintf("System %T is also an UI system. Must be removed via RemoveUISystem.", sys))
3✔
93
        }
94
        s.toRemove = append(s.toRemove, sys)
7✔
95
        if !s.locked {
11✔
96
                s.removeSystems()
4✔
97
        }
4✔
98
}
99

100
// RemoveUISystem removes an UI system from the model.
101
//
102
// Systems can also be removed during a model run.
103
// However, this will take effect only after the end of the full model step.
104
func (s *Systems) RemoveUISystem(sys UISystem) {
12✔
105
        s.uiToRemove = append(s.uiToRemove, sys)
12✔
106
        if !s.locked {
21✔
107
                s.removeSystems()
9✔
108
        }
9✔
109
}
110

111
// Removes systems that were removed during the model step.
112
func (s *Systems) removeSystems() {
1,358✔
113
        for _, sys := range s.toRemove {
1,368✔
114
                s.removeSystem(sys)
10✔
115
        }
10✔
116
        for _, sys := range s.uiToRemove {
1,361✔
117
                if sys, ok := sys.(System); ok {
15✔
118
                        s.removeSystem(sys)
6✔
119
                }
6✔
120
                s.removeUISystem(sys)
6✔
121
        }
122
        s.toRemove = s.toRemove[:0]
1,349✔
123
        s.uiToRemove = s.uiToRemove[:0]
1,349✔
124
}
125

126
func (s *Systems) removeSystem(sys System) {
19✔
127
        if s.locked {
22✔
128
                panic("can't remove a system in locked state")
3✔
129
        }
130
        idx := -1
16✔
131
        for i := 0; i < len(s.systems); i++ {
44✔
132
                if sys == s.systems[i] {
35✔
133
                        idx = i
7✔
134
                        break
7✔
135
                }
136
        }
137
        if idx < 0 {
25✔
138
                panic(fmt.Sprintf("can't remove system %T: not in the model", sys))
9✔
139
        }
140
        s.systems[idx].Finalize(s.world)
7✔
141
        s.systems = append(s.systems[:idx], s.systems[idx+1:]...)
7✔
142
}
143

144
func (s *Systems) removeUISystem(sys UISystem) {
9✔
145
        if s.locked {
12✔
146
                panic("can't remove a system in locked state")
3✔
147
        }
148
        idx := -1
6✔
149
        for i := 0; i < len(s.uiSystems); i++ {
12✔
150
                if sys == s.uiSystems[i] {
12✔
151
                        idx = i
6✔
152
                        break
6✔
153
                }
154
        }
155
        if idx < 0 {
6✔
156
                panic(fmt.Sprintf("can't remove UI system %T: not in the model", sys))
×
157
        }
158
        s.uiSystems[idx].FinalizeUI(s.world)
6✔
159
        s.uiSystems = append(s.uiSystems[:idx], s.uiSystems[idx+1:]...)
6✔
160
}
161

162
// Initialize all systems.
163
func (s *Systems) initialize() {
25✔
164
        if s.initialized {
28✔
165
                panic("model is already initialized")
3✔
166
        }
167

168
        if s.FPS == 0 {
23✔
169
                s.FPS = 30
1✔
170
        }
1✔
171

172
        s.tickRes = generic.NewResource[resource.Tick](s.world)
22✔
173
        s.termRes = generic.NewResource[resource.Termination](s.world)
22✔
174

22✔
175
        s.locked = true
22✔
176
        for _, sys := range s.systems {
53✔
177
                sys.Initialize(s.world)
31✔
178
        }
31✔
179
        for _, sys := range s.uiSystems {
32✔
180
                sys.InitializeUI(s.world)
10✔
181
        }
10✔
182
        s.locked = false
22✔
183
        s.removeSystems()
22✔
184
        s.initialized = true
22✔
185

22✔
186
        s.nextDraw = time.Time{}
22✔
187
        s.nextUpdate = time.Time{}
22✔
188
}
189

190
// Update all systems.
191
func (s *Systems) update() {
1,301✔
192
        s.locked = true
1,301✔
193
        update := s.updateSystems()
1,301✔
194
        s.updateUISystems(update)
1,301✔
195
        s.locked = false
1,301✔
196

1,301✔
197
        s.removeSystems()
1,301✔
198

1,301✔
199
        if update {
2,581✔
200
                time := s.tickRes.Get()
1,280✔
201
                time.Tick++
1,280✔
202
        } else {
1,301✔
203
                s.wait()
21✔
204
        }
21✔
205
}
206

207
// Calculates and waits the time until the next update of UI update.
208
func (s *Systems) wait() {
21✔
209
        var nextUpdate time.Time
21✔
210
        if s.TPS > 0 {
42✔
211
                nextUpdate = s.nextUpdate
21✔
212
        }
21✔
213
        if (s.Paused || s.FPS > 0) && s.nextDraw.Before(nextUpdate) {
33✔
214
                nextUpdate = s.nextDraw
12✔
215
        }
12✔
216
        if nextUpdate.IsZero() {
21✔
217
                return
×
218
        }
×
219

220
        t := time.Now()
21✔
221
        wait := nextUpdate.Sub(t)
21✔
222
        // Wait only if time is sufficiently long, as time.Sleep only guaranties minimum waiting time.
21✔
223
        if wait > time.Millisecond {
42✔
224
                time.Sleep(wait)
21✔
225
        }
21✔
226
}
227

228
// Update normal systems.
229
func (s *Systems) updateSystems() bool {
1,301✔
230
        if s.Paused {
1,301✔
231
                return false
×
232
        }
×
233
        update := false
1,301✔
234
        if s.TPS <= 0 {
2,566✔
235
                update = true
1,265✔
236
                for _, sys := range s.systems {
2,602✔
237
                        sys.Update(s.world)
1,337✔
238
                }
1,337✔
239
        } else {
36✔
240
                update = !time.Now().Before(s.nextUpdate)
36✔
241
                if update {
51✔
242
                        s.nextUpdate = nextTime(s.nextUpdate, s.TPS)
15✔
243
                        for _, sys := range s.systems {
30✔
244
                                sys.Update(s.world)
15✔
245
                        }
15✔
246
                }
247
        }
248
        return update
1,301✔
249
}
250

251
// Update UI systems.
252
func (s *Systems) updateUISystems(updated bool) {
1,301✔
253
        if len(s.uiSystems) > 0 {
1,372✔
254
                if !s.Paused && s.FPS <= 0 {
79✔
255
                        if updated {
13✔
256
                                for _, sys := range s.uiSystems {
10✔
257
                                        sys.UpdateUI(s.world)
5✔
258
                                }
5✔
259
                                for _, sys := range s.uiSystems {
10✔
260
                                        sys.PostUpdateUI(s.world)
5✔
261
                                }
5✔
262
                        }
263
                } else {
63✔
264
                        if !time.Now().Before(s.nextDraw) {
93✔
265
                                fps := s.FPS
30✔
266
                                if s.Paused {
30✔
267
                                        fps = 30
×
268
                                }
×
269
                                s.nextDraw = nextTime(s.nextDraw, fps)
30✔
270
                                for _, sys := range s.uiSystems {
66✔
271
                                        sys.UpdateUI(s.world)
36✔
272
                                }
36✔
273
                                for _, sys := range s.uiSystems {
66✔
274
                                        sys.PostUpdateUI(s.world)
36✔
275
                                }
36✔
276
                        }
277
                }
278
        }
279
}
280

281
// Finalize all systems.
282
func (s *Systems) finalize() {
22✔
283
        s.locked = true
22✔
284
        for _, sys := range s.systems {
50✔
285
                sys.Finalize(s.world)
28✔
286
        }
28✔
287
        for _, sys := range s.uiSystems {
29✔
288
                sys.FinalizeUI(s.world)
7✔
289
        }
7✔
290
        s.locked = false
22✔
291
        s.removeSystems()
22✔
292
}
293

294
// Run the model.
295
func (s *Systems) run() {
22✔
296
        if !s.initialized {
44✔
297
                s.initialize()
22✔
298
        }
22✔
299

300
        time := s.tickRes.Get()
22✔
301
        time.Tick = 0
22✔
302
        terminate := s.termRes.Get()
22✔
303

22✔
304
        for !terminate.Terminate {
1,323✔
305
                s.update()
1,301✔
306
        }
1,301✔
307

308
        s.finalize()
22✔
309
}
310

311
// Removes all systems.
312
func (s *Systems) reset() {
16✔
313
        s.systems = []System{}
16✔
314
        s.uiSystems = []UISystem{}
16✔
315
        s.toRemove = []System{}
16✔
316
        s.uiToRemove = []UISystem{}
16✔
317

16✔
318
        s.nextDraw = time.Time{}
16✔
319
        s.nextUpdate = time.Time{}
16✔
320

16✔
321
        s.initialized = false
16✔
322
        s.tickRes = generic.Resource[resource.Tick]{}
16✔
323
}
16✔
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