197 lines
4.2 KiB
Go
197 lines
4.2 KiB
Go
package ui
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"runtime/debug"
|
|
"runtime/pprof"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
|
)
|
|
|
|
type Game interface {
|
|
Update(screenX, screenY int) error
|
|
Draw(*ebiten.Image) error
|
|
}
|
|
|
|
type CustomCursor interface {
|
|
// The cursor draw operation
|
|
Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error)
|
|
}
|
|
|
|
var (
|
|
screenScale = flag.Float64("screen-scale", 1.0, "Scale the window by this factor")
|
|
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
|
|
)
|
|
|
|
// TODO: move all scaling into Window, so drivers only need to cope with one
|
|
// coordinate space. This will allow us to draw custom mouse cursors in the
|
|
// window, rather than in the driver.
|
|
type Window struct {
|
|
Title string
|
|
KeyUpHandlers map[ebiten.Key]func()
|
|
MouseWheelHandler func(float64, float64)
|
|
MouseClickHandler func()
|
|
|
|
WhileKeyDownHandlers map[ebiten.Key]func()
|
|
|
|
// Allow the "game" to be switched out at any time
|
|
game Game
|
|
|
|
debug bool
|
|
firstRun bool
|
|
|
|
xRes int
|
|
yRes int
|
|
}
|
|
|
|
// 0,0 is the *top left* of the window
|
|
//
|
|
// ebiten assumes a single window, so only call this once...
|
|
func NewWindow(game Game, title string, xRes int, yRes int) (*Window, error) {
|
|
return &Window{
|
|
Title: title,
|
|
debug: true,
|
|
firstRun: true,
|
|
game: game,
|
|
xRes: xRes,
|
|
yRes: yRes,
|
|
|
|
WhileKeyDownHandlers: make(map[ebiten.Key]func()),
|
|
|
|
KeyUpHandlers: make(map[ebiten.Key]func()),
|
|
}, nil
|
|
}
|
|
|
|
// TODO: multiple handlers for the same key?
|
|
func (w *Window) OnKeyUp(key ebiten.Key, f func()) {
|
|
w.KeyUpHandlers[key] = f
|
|
}
|
|
|
|
func (w *Window) WhileKeyDown(key ebiten.Key, f func()) {
|
|
w.WhileKeyDownHandlers[key] = f
|
|
}
|
|
|
|
func (w *Window) OnMouseWheel(f func(x, y float64)) {
|
|
w.MouseWheelHandler = f
|
|
}
|
|
|
|
func (w *Window) OnMouseClick(f func()) {
|
|
w.MouseClickHandler = f
|
|
}
|
|
|
|
func (w *Window) Layout(_, _ int) (int, int) {
|
|
return w.xRes, w.yRes
|
|
}
|
|
|
|
func (w *Window) drawCursor(screen *ebiten.Image) error {
|
|
cIface, ok := w.game.(CustomCursor)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
cursor, op, err := cIface.Cursor()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Hide the system cursor if we have a custom one
|
|
if cursor == nil {
|
|
ebiten.SetCursorMode(ebiten.CursorModeVisible)
|
|
return nil
|
|
}
|
|
|
|
ebiten.SetCursorMode(ebiten.CursorModeHidden)
|
|
|
|
screen.DrawImage(cursor, op)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *Window) Update() (outErr error) {
|
|
// Ebiten does not like it if we panic inside its main loop
|
|
defer func() {
|
|
if panicErr := recover(); panicErr != nil {
|
|
if w.debug {
|
|
debug.PrintStack()
|
|
}
|
|
|
|
outErr = fmt.Errorf("Panic: %v", panicErr)
|
|
}
|
|
}()
|
|
|
|
// FIXME: remove need for update generally
|
|
if err := w.game.Update(w.xRes, w.yRes); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Process keys.
|
|
// FIXME: : should this happen before or after update?
|
|
// TODO: efficient set operations
|
|
|
|
for key, cb := range w.KeyUpHandlers {
|
|
if inpututil.IsKeyJustReleased(key) {
|
|
cb()
|
|
}
|
|
}
|
|
|
|
for key, cb := range w.WhileKeyDownHandlers {
|
|
if ebiten.IsKeyPressed(key) {
|
|
cb()
|
|
}
|
|
}
|
|
|
|
if w.MouseWheelHandler != nil {
|
|
x, y := ebiten.Wheel()
|
|
if x != 0 || y != 0 {
|
|
w.MouseWheelHandler(x, y)
|
|
}
|
|
}
|
|
|
|
if w.MouseClickHandler != nil {
|
|
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
|
|
w.MouseClickHandler()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *Window) Draw(screen *ebiten.Image) {
|
|
w.game.Draw(screen)
|
|
|
|
if w.debug {
|
|
// Draw FPS, etc, to the screen
|
|
msg := fmt.Sprintf("tps=%0.2f fps=%0.2f", ebiten.CurrentTPS(), ebiten.CurrentFPS())
|
|
ebitenutil.DebugPrint(screen, msg)
|
|
}
|
|
|
|
// Draw the cursor last
|
|
w.drawCursor(screen)
|
|
}
|
|
|
|
// TODO: a stop or other cancellation mechanism
|
|
//
|
|
// Note that this must be called on the main OS thread
|
|
func (w *Window) Run() error {
|
|
if *cpuprofile != "" {
|
|
f, err := os.Create(*cpuprofile)
|
|
if err != nil {
|
|
log.Fatal("could not create CPU profile: ", err)
|
|
}
|
|
defer f.Close() // error handling omitted for example
|
|
if err := pprof.StartCPUProfile(f); err != nil {
|
|
log.Fatal("could not start CPU profile: ", err)
|
|
}
|
|
defer pprof.StopCPUProfile()
|
|
}
|
|
|
|
ebiten.SetWindowSize(int(float64(w.xRes)*(*screenScale)), int(float64(w.yRes)*(*screenScale)))
|
|
ebiten.SetWindowTitle(w.Title)
|
|
return ebiten.RunGame(w)
|
|
}
|