package ui import ( "flag" "fmt" "log" "os" "runtime/debug" "runtime/pprof" "github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/ebitenutil" "github.com/hajimehoshi/ebiten/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) { ebiten.SetRunnableInBackground(true) 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) return screen.DrawImage(cursor, op) } func (w *Window) Update(screen *ebiten.Image) (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) } }() if err := w.game.Update(screen.Size()); 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() } } if ebiten.IsDrawingSkipped() { return nil } if err := w.game.Draw(screen); err != nil { return err } 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 return 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) }