diff --git a/internal/game/game.go b/internal/game/game.go index e66c9f0..a0c1bd2 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -27,6 +27,7 @@ type Game struct { fbSize itype.Vec2i io imgui.IO + gui guiState paused bool } diff --git a/internal/game/imgui.go b/internal/game/imgui.go new file mode 100644 index 0000000..e7d3603 --- /dev/null +++ b/internal/game/imgui.go @@ -0,0 +1,122 @@ +package game + +import ( + "fmt" + "log" + "os" + "runtime" + + "edgaru089.ml/go/gl01/internal/asset" + "edgaru089.ml/go/gl01/internal/igwrap" + "edgaru089.ml/go/gl01/internal/util/itype" + "edgaru089.ml/go/gl01/internal/world" + "github.com/go-gl/glfw/v3.3/glfw" + "github.com/inkyblackness/imgui-go/v4" +) + +type guiState struct { + showLog, showDebugInfo bool + logFollow bool + + loadChunkFile string + loadChunkID [2]int32 + + saveChunkFile string + saveChunkID [2]int32 +} + +func (g *Game) initImgui(win *glfw.Window) { + imgui.CreateContext(nil) + g.io = imgui.CurrentIO() + + cfg := imgui.NewFontConfig() + cfg.SetOversampleH(1) + cfg.SetOversampleV(1) + cfg.SetPixelSnapH(true) + g.io.Fonts().AddFontFromMemoryTTFV(asset.Unifont, 16, cfg, g.io.Fonts().GlyphRangesChineseFull()) + igwrap.Init(win) + + g.gui = guiState{ + showLog: true, + showDebugInfo: false, + logFollow: true, + loadChunkFile: "chunk.gob", + loadChunkID: [2]int32{0, 0}, + saveChunkFile: "chunk.gob", + saveChunkID: [2]int32{0, 0}, + } +} + +func (g *Game) imgui() { + + if imgui.BeginV("Player", nil, imgui.WindowFlagsAlwaysAutoResize) { + pos := g.player.Position() + vel := g.player.Speed() + imgui.Text(fmt.Sprintf("Pos: (%.5f, %.5f, %.5f), Vel: (%.5f, %.5f, %.5f)", pos[0], pos[1], pos[2], vel[0], vel[1], vel[2])) + } + imgui.End() + + if imgui.BeginV("Go Runtime", nil, imgui.WindowFlagsAlwaysAutoResize) { + imgui.Text(fmt.Sprintf("%s/%s, compiler: %s", runtime.GOOS, runtime.GOARCH, runtime.Compiler)) + imgui.Text(fmt.Sprintf("NumCPU=%d, NumGOMAXPROCS=%d", runtime.NumCPU(), runtime.GOMAXPROCS(0))) + imgui.Text(fmt.Sprintf("NumCgoCalls=%d, NumGoroutine=%d", runtime.NumCgoCall(), runtime.NumGoroutine())) + imgui.Spacing() + if imgui.ButtonV("!!! PANIC !!!", imgui.Vec2{X: -2, Y: 0}) { + panic("Manual Panic") + } + } + imgui.End() + + if igwrap.Begin("Logs", &g.gui.showLog, imgui.WindowFlagsMenuBar) { + if imgui.BeginMenuBar() { + if imgui.Button("Clear") { + logs = "" + } + if imgui.Button("Add Logs") { + for i := 0; i < 8; i++ { + log.Print("Added logs") + } + } + imgui.Checkbox("Autoscroll", &g.gui.logFollow) + imgui.EndMenuBar() + } + + imgui.BeginChildV("LogScroll", imgui.Vec2{}, true, 0) + imgui.Text(logs) + if g.gui.logFollow && imgui.ScrollY() >= imgui.ScrollMaxY() { + imgui.SetScrollHereY(1.0) + } + imgui.EndChild() + imgui.End() + } + + if imgui.Begin("Actions") { + + imgui.Text("Chunks") + imgui.Separator() + imgui.InputText("Load Filename", &g.gui.loadChunkFile) + imgui.SliderInt2("Load ID", &g.gui.loadChunkID, -10, 10) + if imgui.ButtonV("Load", imgui.Vec2{X: -2, Y: 0}) { + c := &world.Chunk{} + f, err := os.Open(g.gui.loadChunkFile) + if err != nil { + log.Print("LoadChunk: ", err) + } else { + c.LoadFromGobIndexed(f, int(g.gui.loadChunkID[0]), int(g.gui.loadChunkID[1])) + g.world.SetChunk(int(g.gui.loadChunkID[0]), int(g.gui.loadChunkID[1]), c) + } + } + imgui.Separator() + imgui.InputText("Save Filename", &g.gui.saveChunkFile) + imgui.SliderInt2("Save ID", &g.gui.saveChunkID, -10, 10) + if imgui.ButtonV("Save", imgui.Vec2{X: -2, Y: 0}) { + c := g.world.Chunks[itype.Vec2i{int(g.gui.saveChunkID[0]), int(g.gui.saveChunkID[1])}] + f, _ := os.Create(g.gui.saveChunkFile) + c.WriteToGob(f) + f.Close() + } + imgui.Separator() + + } + imgui.End() +} diff --git a/internal/game/logic.go b/internal/game/logic.go index 0cfb264..a7f7edc 100644 --- a/internal/game/logic.go +++ b/internal/game/logic.go @@ -1,14 +1,11 @@ package game import ( - "fmt" "image/color" "log" "os" - "runtime" "time" - "edgaru089.ml/go/gl01/internal/asset" "edgaru089.ml/go/gl01/internal/igwrap" "edgaru089.ml/go/gl01/internal/io" "edgaru089.ml/go/gl01/internal/render" @@ -23,28 +20,6 @@ import ( var logs string -type actions struct { - loadChunkFile string - loadChunkID [2]int32 - - saveChunkFile string - saveChunkID [2]int32 -} - -var action *actions -var logFollow *bool - -func init() { - action = &actions{ - loadChunkFile: "chunk.gob", - loadChunkID: [2]int32{0, 0}, - saveChunkFile: "chunk.gob", - saveChunkID: [2]int32{0, 0}, - } - logFollow = new(bool) - (*logFollow) = true -} - type logger struct{} func (logger) Write(b []byte) (n int, err error) { @@ -103,15 +78,7 @@ func (g *Game) Init(win *glfw.Window) { panic(err) } - imgui.CreateContext(nil) - g.io = imgui.CurrentIO() - - cfg := imgui.NewFontConfig() - cfg.SetOversampleH(1) - cfg.SetOversampleV(1) - cfg.SetPixelSnapH(true) - g.io.Fonts().AddFontFromMemoryTTFV(asset.Unifont, 16, cfg, g.io.Fonts().GlyphRangesChineseFull()) - igwrap.Init(win) + g.initImgui(win) win.SetCursorPos(float64(width)/2, float64(height)/2) @@ -249,78 +216,7 @@ func (g *Game) Update(win *glfw.Window, delta time.Duration) { g.player.SetSpeed(itype.Vec3d{}) } - if imgui.BeginV("Player", nil, imgui.WindowFlagsAlwaysAutoResize) { - pos := g.player.Position() - vel := g.player.Speed() - imgui.Text(fmt.Sprintf("Pos: (%.5f, %.5f, %.5f), Vel: (%.5f, %.5f, %.5f)", pos[0], pos[1], pos[2], vel[0], vel[1], vel[2])) - } - imgui.End() - - if imgui.BeginV("Go Runtime", nil, imgui.WindowFlagsAlwaysAutoResize) { - imgui.Text(fmt.Sprintf("%s/%s, compiler: %s", runtime.GOOS, runtime.GOARCH, runtime.Compiler)) - imgui.Text(fmt.Sprintf("NumCPU=%d, NumGOMAXPROCS=%d", runtime.NumCPU(), runtime.GOMAXPROCS(0))) - imgui.Text(fmt.Sprintf("NumCgoCalls=%d, NumGoroutine=%d", runtime.NumCgoCall(), runtime.NumGoroutine())) - imgui.Spacing() - if imgui.ButtonV("!!! PANIC !!!", imgui.Vec2{X: -2, Y: 0}) { - panic("Manual Panic") - } - } - imgui.End() - - if imgui.BeginV("Logs", nil, imgui.WindowFlagsMenuBar) { - if imgui.BeginMenuBar() { - if imgui.Button("Clear") { - logs = "" - } - if imgui.RadioButton("Follow", *logFollow) { - (*logFollow) = !(*logFollow) - } - imgui.EndMenuBar() - } - - var size *float32 - if size == nil { - size = new(float32) - } - imgui.BeginChildV("LogScroll", imgui.Vec2{}, true, 0) - imgui.Text(logs) - if (*size) != imgui.ScrollMaxY() && (*logFollow) { - imgui.SetScrollY(imgui.ScrollMaxY()) - } - (*size) = imgui.ScrollMaxY() - imgui.EndChild() - } - imgui.End() - - if imgui.Begin("Actions") { - - imgui.Text("Chunks") - imgui.Separator() - imgui.InputText("Load Filename", &action.loadChunkFile) - imgui.SliderInt2("Load ID", &action.loadChunkID, -10, 10) - if imgui.ButtonV("Load", imgui.Vec2{X: -2, Y: 0}) { - c := &world.Chunk{} - f, err := os.Open(action.loadChunkFile) - if err != nil { - log.Print("LoadChunk: ", err) - } else { - c.LoadFromGobIndexed(f, int(action.loadChunkID[0]), int(action.loadChunkID[1])) - g.world.SetChunk(int(action.loadChunkID[0]), int(action.loadChunkID[1]), c) - } - } - imgui.Separator() - imgui.InputText("Save Filename", &action.saveChunkFile) - imgui.SliderInt2("Save ID", &action.saveChunkID, -10, 10) - if imgui.ButtonV("Save", imgui.Vec2{X: -2, Y: 0}) { - c := g.world.Chunks[itype.Vec2i{int(action.saveChunkID[0]), int(action.saveChunkID[1])}] - f, _ := os.Create(action.saveChunkFile) - c.WriteToGob(f) - f.Close() - } - imgui.Separator() - - } - imgui.End() + g.imgui() } // Render, called with a OpenGL context, renders the game. diff --git a/internal/igwrap/color.go b/internal/igwrap/color.go new file mode 100644 index 0000000..13b433b --- /dev/null +++ b/internal/igwrap/color.go @@ -0,0 +1,13 @@ +package igwrap + +import "github.com/inkyblackness/imgui-go/v4" + +// Color converts non-premultiplied RGBA color into imgui.Vec4. +func Color(r, g, b, a uint8) imgui.Vec4 { + return imgui.Vec4{ + X: float32(r) / 255, + Y: float32(g) / 255, + Z: float32(b) / 255, + W: float32(a) / 255, + } +} diff --git a/internal/igwrap/glyph_ranges.go b/internal/igwrap/glyph_ranges.go new file mode 100644 index 0000000..0a6438d --- /dev/null +++ b/internal/igwrap/glyph_ranges.go @@ -0,0 +1,24 @@ +package igwrap + +import "github.com/inkyblackness/imgui-go/v4" + +var glyphRanges imgui.AllocatedGlyphRanges + +// GlyphRanges returns a custom-built glyph ranges set. +// +// The ImGUI context must be already initialized. +func GlyphRanges() imgui.GlyphRanges { + if glyphRanges.GlyphRanges == 0 { + b := &imgui.GlyphRangesBuilder{} + + b.AddExisting(imgui.CurrentIO().Fonts().GlyphRangesChineseFull()) + + // Greek + b.Add(0x0391, 0x03A1) + b.Add(0x03A3, 0x03FF) + + glyphRanges = b.Build() + } + + return glyphRanges.GlyphRanges +} diff --git a/internal/igwrap/wrap.go b/internal/igwrap/wrap.go new file mode 100644 index 0000000..24a4286 --- /dev/null +++ b/internal/igwrap/wrap.go @@ -0,0 +1,205 @@ +package igwrap + +import ( + "fmt" + + "edgaru089.ml/go/gl01/internal/util/itype" + "github.com/inkyblackness/imgui-go/v4" +) + +// MenuItem wraps imgui.MenuItemV to create a +// easy-to-use and intuitive interface. +func MenuItem(name, shortcut string, selected *bool, enabled bool) { + if imgui.MenuItemV(name, shortcut, *selected, enabled) { + (*selected) = !(*selected) + } +} + +// Text wraps imgui.Text to create a +// shortcut for fmt.Sprintf. +func Text(format string, a ...interface{}) { + imgui.Text(fmt.Sprintf(format, a...)) +} + +// Begin wraps imgui.BeginV to create a +// easy-to-use and intuitive interface. +// +// You only need to call End() if the function +// returns true. +func Begin(id string, open *bool, flags imgui.WindowFlags) bool { + // skip if the window is not open + if !(*open) { + return false + } + + ok := imgui.BeginV(id, open, flags) + if !ok { + imgui.End() + } + return ok +} + +// Sliderf creates a slider with 1 float32 variable. +func Sliderf(label string, a *float32, min, max float32) bool { + f := *a + ok := imgui.SliderFloat(label, &f, min, max) + (*a) = f + return ok +} + +// Slider2f creates a slider with 2 float32 variables. +func Slider2f(label string, a, b *float32, min, max float32) bool { + f := [2]float32{*a, *b} + ok := imgui.SliderFloat2(label, &f, min, max) + (*a) = f[0] + (*b) = f[1] + return ok +} + +// Slider3f creates a slider with 3 float32 variables. +func Slider3f(label string, a, b, c *float32, min, max float32) bool { + f := [3]float32{*a, *b, *c} + ok := imgui.SliderFloat3(label, &f, min, max) + (*a) = f[0] + (*b) = f[1] + (*c) = f[2] + return ok +} + +// Slider4f creates a slider with 4 float32 variables. +func Slider4f(label string, a, b, c, d *float32, min, max float32) bool { + f := [4]float32{*a, *b, *c, *d} + ok := imgui.SliderFloat4(label, &f, min, max) + (*a) = f[0] + (*b) = f[1] + (*c) = f[2] + (*d) = f[3] + return ok +} + +// Sliderd creates a slider with 1 float64 variable. +func Sliderd(label string, a *float64, min, max float64) bool { + f := float32(*a) + ok := imgui.SliderFloat(label, &f, float32(min), float32(max)) + (*a) = float64(f) + return ok +} + +// Slider2d creates a slider with 2 float64 variables. +func Slider2d(label string, a, b *float64, min, max float64) bool { + f := [2]float32{float32(*a), float32(*b)} + ok := imgui.SliderFloat2(label, &f, float32(min), float32(max)) + (*a) = float64(f[0]) + (*b) = float64(f[1]) + return ok +} + +// Slider3d creates a slider with 3 float64 variables. +func Slider3d(label string, a, b, c *float64, min, max float64) bool { + f := [3]float32{float32(*a), float32(*b), float32(*c)} + ok := imgui.SliderFloat3(label, &f, float32(min), float32(max)) + (*a) = float64(f[0]) + (*b) = float64(f[1]) + (*c) = float64(f[2]) + return ok +} + +// Slider4d creates a slider with 4 float64 variables. +func Slider4d(label string, a, b, c, d *float64, min, max float64) bool { + f := [4]float32{float32(*a), float32(*b), float32(*c), float32(*d)} + ok := imgui.SliderFloat4(label, &f, float32(min), float32(max)) + (*a) = float64(f[0]) + (*b) = float64(f[1]) + (*c) = float64(f[2]) + (*d) = float64(f[3]) + return ok +} + +// DragVec2d creates a dragger with a itype.Vec2d. +func DragVec2d(label string, vec *itype.Vec2d, speed float64) bool { + f := [2]float32{float32((*vec)[0]), float32((*vec)[1])} + ok := imgui.DragFloat2V(label, &f, float32(speed), 0, 0, "%.3f", imgui.SliderFlagsNoRoundToFormat) + (*vec)[0] = float64(f[0]) + (*vec)[1] = float64(f[1]) + return ok +} + +// DragVec2dv creates a dragger with two float64s. +func DragVec2dv(label string, a, b *float64, speed float64) bool { + f := [2]float32{float32(*a), float32(*b)} + ok := imgui.DragFloat2V(label, &f, float32(speed), 0, 0, "%.3f", imgui.SliderFlagsNoRoundToFormat) + (*a) = float64(f[0]) + (*b) = float64(f[1]) + return ok +} + +// Dragd creates a dragger with one float64. +func Dragd(label string, a *float64, speed float64) bool { + f := float32(*a) + ok := imgui.DragFloatV(label, &f, float32(speed), 0, 0, "%.3f", imgui.SliderFlagsNoRoundToFormat) + (*a) = float64(f) + return ok +} + +// Checkbox wraps imgui.Checkbox. +// +// It returns true when selected. +func Checkbox(label string, selected bool) bool { + imgui.Checkbox(label, &selected) + return selected +} + +// DragButtonV wraps imgui.Button to create a button +// that can be dragged around. +// +// The pushid, if not empty, is pushed into the ImGUI stack. +// +// It returns the mouse delta, if dragged, in the last frame. +func DragButtonV(label string, pushid string) (delta itype.Vec2d) { + if len(pushid) != 0 { + imgui.PushID(pushid) + defer imgui.PopID() + } + + imgui.Button(label) + if imgui.IsItemActive() { + d := imgui.CurrentIO().MouseDelta() + delta = itype.Vec2d{float64(d.X), float64(d.Y)} + } + return +} + +// DragButton calls DragButtonV("Drag", pushid) to create a button +// that can be dragged around. +// +// The pushid, if not empty, is pushed into the ImGUI stack. +// +// It returns the mouse delta, if dragged, in the last frame. +func DragButton(pushid string) (delta itype.Vec2d) { + return DragButtonV("Drag", pushid) +} + +// DragButtonv wraps DragButton to create a button to be dragged around. +// +// The pushid, if not empty, is pushed into the ImGUI stack. +// +// It returns whether the values has changed. +func DragButtonv(pushid string, x, y *float64, speed float64) bool { + delta := DragButton(pushid).Multiply(speed) + if delta == (itype.Vec2d{}) { + return false + } + (*x) += delta[0] + (*y) += delta[1] + return true +} + +// InputInt calls imgui.InputInt. +// +// It accepts int instead of int32. +func InputInt(label string, val *int) bool { + x := int32(*val) + ok := imgui.InputInt(label, &x) + (*val) = int(x) + return ok +} diff --git a/internal/util/itype/vec.go b/internal/util/itype/vec.go index fd2c6a6..877d340 100644 --- a/internal/util/itype/vec.go +++ b/internal/util/itype/vec.go @@ -49,6 +49,33 @@ func (v Vec4i) MultiplyInt(mult int) Vec4i { // Vec2f is a two-element float vector type Vec2f [2]float32 +func (v Vec2f) Add(add Vec2f) Vec2f { + return Vec2f{v[0] + add[0], v[1] + add[1]} +} + +func (v Vec2f) Addv(x, y float32) Vec2f { + return Vec2f{v[0] + x, v[1] + y} +} + +func (v Vec2f) Floor() Vec2i { + return Vec2i{ + int(math.Floor(float64(v[0]))), + int(math.Floor(float64(v[1]))), + } +} +func (v Vec2f) Length() float32 { + return float32(math.Sqrt(float64(v[0]*v[0] + v[1]*v[1]))) +} + +func (v Vec2f) Normalize() Vec2f { + l := v.Length() + return Vec2f{v[0] / l, v[1] / l} +} + +func (v Vec2f) ToFloat64() Vec2d { + return Vec2d{float64(v[0]), float64(v[1])} +} + // Vec3f is a three-element float vector type Vec3f [3]float32 @@ -91,6 +118,37 @@ type Vec4f [4]float32 // Vec2d is a two-element float64 vector type Vec2d [2]float64 +func (v Vec2d) Add(add Vec2d) Vec2d { + return Vec2d{v[0] + add[0], v[1] + add[1]} +} + +func (v Vec2d) Addv(x, y float64) Vec2d { + return Vec2d{v[0] + x, v[1] + y} +} + +func (v Vec2d) Multiply(x float64) Vec2d { + return Vec2d{v[0] * x, v[1] * x} +} + +func (v Vec2d) Floor() Vec2i { + return Vec2i{ + int(math.Floor(float64(v[0]))), + int(math.Floor(float64(v[1]))), + } +} +func (v Vec2d) Length() float64 { + return math.Sqrt(v[0]*v[0] + v[1]*v[1]) +} + +func (v Vec2d) Normalize() Vec2d { + l := v.Length() + return Vec2d{v[0] / l, v[1] / l} +} + +func (v Vec2d) ToFloat32() Vec2f { + return Vec2f{float32(v[0]), float32(v[1])} +} + // Vec3d is a three-element float64 vector type Vec3d [3]float64