Skip to content

Commit 427ce67

Browse files
authored
Entity inspector (#21)
* add inspector drawer for entity inspection * allow to toggle types, names, etc. using the keyboard * add feature list to README, update screenshot, update dependencies
1 parent 5fbecbd commit 427ce67

File tree

6 files changed

+184
-36
lines changed

6 files changed

+184
-36
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [[v0.2.0]](https://github.com/mlange-42/arche-pixel/compare/v0.1.0...v0.2.0)
4+
5+
### Features
6+
7+
* New drawer `Inspector` for inspecting entities (#21)
8+
39
## [[v0.1.0]](https://github.com/mlange-42/arche-pixel/compare/v0.0.3...v0.1.0)
410

511
### Features

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,20 @@
88

99
*Arche Pixel* provides OpenGL graphics and live plots for the [Arche](https://github.com/mlange-42/arche) Entity Component System (ECS) using the [Pixel](https://github.com/faiface/pixel) game engine.
1010

11-
![Screenshot](https://user-images.githubusercontent.com/44003176/230742745-d7adee93-8c70-4e32-b031-7258185aa2b1.png)
11+
<div align="center" width="100%">
12+
13+
![Screenshot](https://user-images.githubusercontent.com/44003176/232126308-60299642-0490-478d-82a5-48d862da6703.png)
14+
*Screenshot showing Arche Pixel features, visualizing an evolutionary forest model.*
15+
</div>
16+
17+
## Features
18+
19+
* Free 2D drawing using a convenient OpenGL interface.
20+
* Live time series plots using unified observers.
21+
* ECS engine monitor for detailed performance statistics.
22+
* Entity inspector for debugging and inspection.
23+
* Simulation controls to pause or limit speed interactively.
24+
* User input handling for interactive simulations.
1225

1326
## Installation
1427

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ go 1.20
55
require (
66
github.com/faiface/pixel v0.10.0
77
github.com/mazznoer/colorgrad v0.9.1
8-
github.com/mlange-42/arche v0.6.3
9-
github.com/mlange-42/arche-model v0.1.0
8+
github.com/mlange-42/arche v0.7.0
9+
github.com/mlange-42/arche-model v0.2.0
1010
github.com/stretchr/testify v1.8.1
1111
golang.org/x/image v0.5.0
1212
gonum.org/v1/plot v0.12.0

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ github.com/mazznoer/colorgrad v0.9.1 h1:MB80JYVndKWSMEM1beNqnuOowWGhoQc3DXWXkFp6
5151
github.com/mazznoer/colorgrad v0.9.1/go.mod h1:WX2R9wt9B47+txJZVVpM9LY+LAGIdi4lTI5wIyreDH4=
5252
github.com/mazznoer/csscolorparser v0.1.2 h1:/UBHuQg792ePmGFzTQAC9u+XbFr7/HzP/Gj70Phyz2A=
5353
github.com/mazznoer/csscolorparser v0.1.2/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtdHtGfQ32xZ5PJQic=
54-
github.com/mlange-42/arche v0.6.3 h1:VYADSGU2BXBLJtjqN+7UqH2FW0Tmdpnms7TBm/Sa4fM=
55-
github.com/mlange-42/arche v0.6.3/go.mod h1:cgOiMHWCbNAb5L5mCihS2yzdk1Zll1IoBJL+3wVm0AM=
56-
github.com/mlange-42/arche-model v0.1.0 h1:F9PnAruRgxO4LJfzFORVHujNUGhQYk98TDO3IyK+jz0=
57-
github.com/mlange-42/arche-model v0.1.0/go.mod h1:VftJXB24OjILidg0GXPdP8sZbB3wn2eCWx/i9NMIM+I=
54+
github.com/mlange-42/arche v0.7.0 h1:66HmSfVQFIYx7PyQtOjmWuGCzBX0fhefYqpDkWpyy6E=
55+
github.com/mlange-42/arche v0.7.0/go.mod h1:cgOiMHWCbNAb5L5mCihS2yzdk1Zll1IoBJL+3wVm0AM=
56+
github.com/mlange-42/arche-model v0.2.0 h1:JrGKLIGDMKumsYeddKw9S5RlUk0OOzUQH7GkkZGj1YM=
57+
github.com/mlange-42/arche-model v0.2.0/go.mod h1:VSOVJBHjw6gQm9B1vOVLsteFtXvZgJmNCOKQoh1jqYc=
5858
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
5959
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
6060
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=

plot/inspector.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package plot
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"reflect"
7+
8+
px "github.com/faiface/pixel"
9+
"github.com/faiface/pixel/pixelgl"
10+
"github.com/faiface/pixel/text"
11+
"github.com/mlange-42/arche-model/resource"
12+
"github.com/mlange-42/arche/ecs"
13+
"github.com/mlange-42/arche/generic"
14+
)
15+
16+
// Inspector drawer for inspecting entities.
17+
//
18+
// Shows information of the entity indicated by the SelectedEntity resource ([github.com/mlange-42/arche-model/resource.SelectedEntity]).
19+
// Entity selection is to be done by another system, e.g. by user input.
20+
//
21+
// Details can be adjusted using the HideXxx fields.
22+
// Further, keys F, T, V and N can be used to toggle details during a running simulation.
23+
type Inspector struct {
24+
HideFields bool // Hides components fields.
25+
HideTypes bool // Hides field types.
26+
HideValues bool // Hides field values.
27+
HideNames bool // Hide field names of nested structs.
28+
selectedRes generic.Resource[resource.SelectedEntity]
29+
text *text.Text
30+
helpText *text.Text
31+
}
32+
33+
// Initialize the system
34+
func (m *Inspector) Initialize(w *ecs.World, win *pixelgl.Window) {
35+
m.selectedRes = generic.NewResource[resource.SelectedEntity](w)
36+
37+
m.text = text.New(px.V(0, 0), font)
38+
m.helpText = text.New(px.V(0, 0), font)
39+
40+
fmt.Fprint(m.helpText, "Toggle [f]ields, [t]ypes, [v]alues or [n]ames")
41+
}
42+
43+
// Update the drawer.
44+
func (m *Inspector) Update(w *ecs.World) {}
45+
46+
// UpdateInputs handles input events of the previous frame update.
47+
func (m *Inspector) UpdateInputs(w *ecs.World, win *pixelgl.Window) {
48+
if win.JustPressed(pixelgl.KeyF) {
49+
m.HideFields = !m.HideFields
50+
return
51+
}
52+
if win.JustPressed(pixelgl.KeyT) {
53+
m.HideTypes = !m.HideTypes
54+
return
55+
}
56+
if win.JustPressed(pixelgl.KeyV) {
57+
m.HideValues = !m.HideValues
58+
return
59+
}
60+
if win.JustPressed(pixelgl.KeyN) {
61+
m.HideNames = !m.HideNames
62+
return
63+
}
64+
}
65+
66+
// Draw the system
67+
func (m *Inspector) Draw(w *ecs.World, win *pixelgl.Window) {
68+
m.helpText.Draw(win, px.IM.Moved(px.V(10, 10)))
69+
70+
if !m.selectedRes.Has() {
71+
return
72+
}
73+
sel := m.selectedRes.Get().Selected
74+
if sel.IsZero() {
75+
return
76+
}
77+
78+
height := win.Canvas().Bounds().H()
79+
x0 := 10.0
80+
y0 := height - 20.0
81+
82+
m.text.Clear()
83+
fmt.Fprintf(m.text, "Entity %+v\n\n", sel)
84+
85+
if !w.Alive(sel) {
86+
fmt.Fprint(m.text, " dead entity")
87+
m.text.Draw(win, px.IM.Moved(px.V(x0, y0)))
88+
return
89+
}
90+
91+
mask := w.Mask(sel)
92+
bits := mask.TotalBitsSet()
93+
94+
for i := 0; i < ecs.MaskTotalBits && bits > 0; i++ {
95+
id := ecs.ID(i)
96+
if mask.Get(id) {
97+
tp, _ := w.ComponentType(id)
98+
ptr := w.Get(sel, id)
99+
val := reflect.NewAt(tp, ptr).Elem()
100+
101+
fmt.Fprintf(m.text, " %s\n", tp.Name())
102+
103+
if !m.HideFields {
104+
for i := 0; i < val.NumField(); i++ {
105+
m.printField(m.text, tp, tp.Field(i), val.Field(i))
106+
}
107+
fmt.Fprint(m.text, "\n")
108+
}
109+
bits--
110+
}
111+
}
112+
113+
m.text.Draw(win, px.IM.Moved(px.V(x0, y0)))
114+
}
115+
116+
func (m *Inspector) printField(w io.Writer, tp reflect.Type, field reflect.StructField, value reflect.Value) {
117+
fmt.Fprintf(w, " %-20s ", field.Name)
118+
if !m.HideTypes {
119+
fmt.Fprintf(w, " %-16s ", value.Type())
120+
}
121+
if !m.HideValues {
122+
if m.HideNames {
123+
fmt.Fprintf(w, "= %v", value.Interface())
124+
} else {
125+
fmt.Fprintf(w, "= %+v", value.Interface())
126+
}
127+
}
128+
fmt.Fprint(m.text, "\n")
129+
}

plot/perf_stats.go

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,60 +27,60 @@ type PerfStats struct {
2727
}
2828

2929
// Initialize the system
30-
func (m *PerfStats) Initialize(w *ecs.World, win *pixelgl.Window) {
31-
if m.SampleInterval <= 0 {
32-
m.SampleInterval = time.Second
30+
func (p *PerfStats) Initialize(w *ecs.World, win *pixelgl.Window) {
31+
if p.SampleInterval <= 0 {
32+
p.SampleInterval = time.Second
3333
}
34-
m.lastPlotUpdate = time.Now()
35-
m.startTime = m.lastPlotUpdate
34+
p.lastPlotUpdate = time.Now()
35+
p.startTime = p.lastPlotUpdate
3636

37-
m.drawer = *imdraw.New(nil)
37+
p.drawer = *imdraw.New(nil)
3838

39-
m.summary = text.New(px.V(0, 0), font)
39+
p.summary = text.New(px.V(0, 0), font)
4040

41-
m.step = 0
41+
p.step = 0
4242

4343
st := w.Stats()
44-
m.stats.Entities = st.Entities.Used
45-
m.stats.Mem = st.Memory
44+
p.stats.Entities = st.Entities.Used
45+
p.stats.Mem = st.Memory
4646
}
4747

4848
// Update the drawer.
49-
func (m *PerfStats) Update(w *ecs.World) {
49+
func (p *PerfStats) Update(w *ecs.World) {
5050
t := time.Now()
51-
m.frameTimer.Update(m.step, t)
51+
p.frameTimer.Update(p.step, t)
5252

53-
if t.Sub(m.lastPlotUpdate) >= m.SampleInterval {
53+
if t.Sub(p.lastPlotUpdate) >= p.SampleInterval {
5454
st := w.Stats()
55-
m.stats.Entities = st.Entities.Used
56-
m.stats.Mem = st.Memory
57-
m.lastPlotUpdate = t
55+
p.stats.Entities = st.Entities.Used
56+
p.stats.Mem = st.Memory
57+
p.lastPlotUpdate = t
5858
}
5959

60-
m.step++
60+
p.step++
6161
}
6262

6363
// UpdateInputs handles input events of the previous frame update.
64-
func (m *PerfStats) UpdateInputs(w *ecs.World, win *pixelgl.Window) {}
64+
func (p *PerfStats) UpdateInputs(w *ecs.World, win *pixelgl.Window) {}
6565

6666
// Draw the system
67-
func (m *PerfStats) Draw(w *ecs.World, win *pixelgl.Window) {
68-
m.summary.Clear()
69-
mem, units := toMemText(m.stats.Mem)
67+
func (p *PerfStats) Draw(w *ecs.World, win *pixelgl.Window) {
68+
p.summary.Clear()
69+
mem, units := toMemText(p.stats.Mem)
7070
fmt.Fprintf(
71-
m.summary, "Tick: %7d\nEnt.: %7d\nTPS: %8.1f\nTPT: %6.2fms\nMem: %6.1f%s\nTime: %7s",
72-
m.step, m.stats.Entities, m.frameTimer.FPS(),
73-
float64(m.frameTimer.FrameTime().Microseconds())/1000,
74-
mem, units, time.Since(m.startTime).Round(time.Second),
71+
p.summary, "Tick: %7d\nEnt.: %7d\nTPS: %8.1f\nTPT: %6.2fms\nMem: %6.1f%s\nTime: %7s",
72+
p.step, p.stats.Entities, p.frameTimer.FPS(),
73+
float64(p.frameTimer.FrameTime().Microseconds())/1000,
74+
mem, units, time.Since(p.startTime).Round(time.Second),
7575
)
7676

77-
dr := &m.drawer
77+
dr := &p.drawer
7878
height := win.Canvas().Bounds().H()
7979
x0 := 10.0
8080
y0 := height - 20.0
8181

82-
v1 := px.V(x0+m.summary.Bounds().Min.X-5, y0+m.summary.Bounds().Min.Y-5)
83-
v2 := px.V(x0+m.summary.Bounds().Max.X+5, y0+m.summary.Bounds().Max.Y+5)
82+
v1 := px.V(x0+p.summary.Bounds().Min.X-5, y0+p.summary.Bounds().Min.Y-5)
83+
v2 := px.V(x0+p.summary.Bounds().Max.X+5, y0+p.summary.Bounds().Max.Y+5)
8484

8585
dr.Color = color.Black
8686
dr.Push(v1, v2)
@@ -94,7 +94,7 @@ func (m *PerfStats) Draw(w *ecs.World, win *pixelgl.Window) {
9494
dr.Reset()
9595
dr.Clear()
9696

97-
m.summary.Draw(win, px.IM.Moved(px.V(x0, y0)))
97+
p.summary.Draw(win, px.IM.Moved(px.V(x0, y0)))
9898

9999
}
100100

0 commit comments

Comments
 (0)