From 237498e51fbf9a49b323eaed68a11849865ef8d9 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Tue, 24 Mar 2020 12:01:37 +0900 Subject: [PATCH] ui: Add an optional function Draw function to Game interface (#1107) This change adds an optional function Draw to the Game interface. With Draw function, the game logic and rendering are separate. There are some benefits: * The API is clearer and easier to understand. * When TPS < FPS, smoother rendering can be performed without changing the game logic depending on TPS. * Porting to XNA, which has separate functions Update and Draw, would be a little easier. Draw is optional due to backward compatibility. Game interface was already used before v1.11.x in mobile packages, and adding a function would break existing code unfortunately. Then, we adopted switching the behavior based on whether Draw is implemented or not by type assertions. IsDrawingSkipped will always return false when Draw is implemented. Fixes #1104 --- doc.go | 18 +++++------ examples/moire/main.go | 10 +++---- examples/windowsize/main.go | 17 +++++------ imagedumper_desktop.go | 4 +++ imagedumper_notdesktop.go | 5 ++++ run.go | 60 ++++++++++++++++++++++++++++++++++--- uicontext.go | 44 ++++++++++++++++++++------- 7 files changed, 118 insertions(+), 40 deletions(-) diff --git a/doc.go b/doc.go index 93369fcad..7ea10512a 100644 --- a/doc.go +++ b/doc.go @@ -20,22 +20,18 @@ // type Game struct{} // // // Update proceeds the game state. -// // Update is called every frame (1/60 [s]). +// // Update is called every tick (1/60 [s] by default). // func (g *Game) Update(screen *ebiten.Image) error { -// // // Write your game's logical update. -// -// if ebiten.IsDrawingSkipped() { -// // When the game is running slowly, the rendering result -// // will not be adopted. -// return nil -// } -// -// // Write your game's rendering. -// // return nil // } // +// // Draw draws the game screen. +// // Draw is called every frame (typically 1/60[s] for 60Hz display). +// func (g *Game) Update(screen *ebiten.Image) error { +// // Write your game's rendering. +// } +// // // Layout takes the outside size (e.g., the window size) and returns the (logical) screen size. // // If you don't have to adjust the screen size with the outside size, just return a fixed size. // func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { diff --git a/examples/moire/main.go b/examples/moire/main.go index d9a771612..e2f9bd94b 100644 --- a/examples/moire/main.go +++ b/examples/moire/main.go @@ -86,15 +86,13 @@ func (g *game) Update(screen *ebiten.Image) error { fullscreen = !fullscreen ebiten.SetFullscreen(fullscreen) } - - if ebiten.IsDrawingSkipped() { - return nil - } - - screen.ReplacePixels(getDots(screen.Size())) return nil } +func (g *game) Draw(screen *ebiten.Image) { + screen.ReplacePixels(getDots(screen.Size())) +} + func main() { g := &game{ scale: initScreenScale, diff --git a/examples/windowsize/main.go b/examples/windowsize/main.go index 11f77a911..30ae60a01 100644 --- a/examples/windowsize/main.go +++ b/examples/windowsize/main.go @@ -99,8 +99,9 @@ func createRandomIconImage() image.Image { } type game struct { - width int - height int + width int + height int + transparent bool } func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) { @@ -139,7 +140,7 @@ func (g *game) Update(screen *ebiten.Image) error { tps := ebiten.MaxTPS() decorated := ebiten.IsWindowDecorated() positionX, positionY := ebiten.WindowPosition() - transparent := ebiten.IsScreenTransparent() + g.transparent = ebiten.IsScreenTransparent() floating := ebiten.IsWindowFloating() resizable := ebiten.IsWindowResizable() @@ -269,12 +270,11 @@ func (g *game) Update(screen *ebiten.Image) error { } count++ + return nil +} - if ebiten.IsDrawingSkipped() { - return nil - } - - if !transparent { +func (g *game) Draw(screen *ebiten.Image) { + if !g.transparent { screen.Fill(color.RGBA{0x80, 0x80, 0xc0, 0xff}) } w, h := gophersImage.Size() @@ -336,7 +336,6 @@ TPS: Current: %0.2f / Max: %s FPS: %0.2f Device Scale Factor: %0.2f`, msgS, msgM, msgR, fg, wx, wy, cx, cy, ebiten.CurrentTPS(), tpsStr, ebiten.CurrentFPS(), ebiten.DeviceScaleFactor()) ebitenutil.DebugPrint(screen, msg) - return nil } func parseWindowPosition() (int, int, bool) { diff --git a/imagedumper_desktop.go b/imagedumper_desktop.go index ab9b689c7..aa947e222 100644 --- a/imagedumper_desktop.go +++ b/imagedumper_desktop.go @@ -158,6 +158,10 @@ func (i *imageDumper) update(screen *Image) error { return nil } + return i.dump(screen) +} + +func (i *imageDumper) dump(screen *Image) error { if i.toTakeScreenshot { i.toTakeScreenshot = false if err := takeScreenshot(screen); err != nil { diff --git a/imagedumper_notdesktop.go b/imagedumper_notdesktop.go index 5bf8c2a38..bea6f382d 100644 --- a/imagedumper_notdesktop.go +++ b/imagedumper_notdesktop.go @@ -23,3 +23,8 @@ type imageDumper struct { func (i *imageDumper) update(screen *Image) error { return i.f(screen) } + +func (i *imageDumper) dump(screen *Image) error { + // Do nothing + return nil +} diff --git a/run.go b/run.go index 5bfc6afa0..a08ae0a40 100644 --- a/run.go +++ b/run.go @@ -23,9 +23,22 @@ import ( // Game defines necessary functions for a game. type Game interface { - // Update updates a game by one frame. + // Update updates a game by one tick. + // + // Basically Update updates the game logic, and whether Update draws the screen depends on the existence of + // Draw implementation. + // + // With Draw, Update updates only the game logic and Draw draws the screen. This is recommended. + // + // Without Draw, Update updates the game logic and also draws the screen. This is a legacy way. Update(*Image) error + // Draw draws the game screen by one frame. + // + // Draw is an optional function for backward compatibility. + // + // Draw(*Image) error + // Layout accepts a native outside size in device-independent pixels and returns the game's logical screen // size. // @@ -94,13 +107,16 @@ func setDrawingSkipped(skipped bool) { // return nil // } // +// IsDrawingSkipped is useful if you use Run function or RunGame function without implementing Game's Draw. +// Otherwise, i.e., if you use RunGame function with implementing Game's Draw, IsDrawingSkipped should not be used. +// If you use RunGame and Draw, IsDrawingSkipped always returns true. +// // IsDrawingSkipped is concurrent-safe. func IsDrawingSkipped() bool { return atomic.LoadInt32(&isDrawingSkipped) != 0 } -// IsRunningSlowly is deprecated as of 1.8.0-alpha. -// Use IsDrawingSkipped instead. +// IsRunningSlowly is deprecated as of 1.8.0-alpha. Use Game's Draw function instead. func IsRunningSlowly() bool { return IsDrawingSkipped() } @@ -184,10 +200,41 @@ func (i *imageDumperGame) Layout(outsideWidth, outsideHeight int) (screenWidth, return i.game.Layout(outsideWidth, outsideHeight) } +type imageDumperGameWithDraw struct { + imageDumperGame + err error +} + +func (i *imageDumperGameWithDraw) Update(screen *Image) error { + if i.err != nil { + return i.err + } + return i.imageDumperGame.Update(screen) +} + +func (i *imageDumperGameWithDraw) Draw(screen *Image) { + if i.err != nil { + return + } + + i.game.(interface{ Draw(*Image) }).Draw(screen) + + // Call dump explicitly. IsDrawingSkipped always returns true when Draw is defined. + if i.d == nil { + i.d = &imageDumper{f: i.game.Update} + } + i.err = i.d.dump(screen) +} + // RunGame starts the main loop and runs the game. -// game's Update function is called every frame. +// game's Update function is called every tick to update the gmae logic. +// game's Draw function is, if it exists, called every frame to draw the screen. // game's Layout function is called when necessary, and you can specify the logical screen size by the function. // +// game must implement Game interface. +// Game's Draw function is optional, but it is recommended to implement Draw to seperate updating the logic and +// rendering. +// // RunGame is a more flexibile form of Run due to 'Layout' function. // You can make a resizable window if you use RunGame, while you cannot if you use Run. // RunGame is more sophisticated way than Run and hides the notion of 'scale'. @@ -224,6 +271,11 @@ func (i *imageDumperGame) Layout(outsideWidth, outsideHeight int) (screenWidth, // Don't call RunGame twice or more in one process. func RunGame(game Game) error { fixWindowPosition(WindowSize()) + if _, ok := game.(interface{ Draw(*Image) }); ok { + return runGame(&imageDumperGameWithDraw{ + imageDumperGame: imageDumperGame{game: game}, + }, 0) + } return runGame(&imageDumperGame{game: game}, 0) } diff --git a/uicontext.go b/uicontext.go index 1ec3657db..1bb8cc774 100644 --- a/uicontext.go +++ b/uicontext.go @@ -248,22 +248,46 @@ func (c *uiContext) Update(afterFrameUpdate func()) error { func (c *uiContext) update(afterFrameUpdate func()) error { updateCount := clock.Update(MaxTPS()) - for i := 0; i < updateCount; i++ { - c.updateOffscreen() + + if game, ok := c.game.(interface{ Draw(*Image) }); ok { + for i := 0; i < updateCount; i++ { + c.updateOffscreen() + + // Rendering should be always skipped. + setDrawingSkipped(true) + + if err := hooks.RunBeforeUpdateHooks(); err != nil { + return err + } + if err := c.game.Update(c.offscreen); err != nil { + return err + } + uiDriver().Input().ResetForFrame() + afterFrameUpdate() + } // Mipmap images should be disposed by Clear. c.offscreen.Clear() - setDrawingSkipped(i < updateCount-1) + game.Draw(c.offscreen) + } else { + for i := 0; i < updateCount; i++ { + c.updateOffscreen() - if err := hooks.RunBeforeUpdateHooks(); err != nil { - return err + // Mipmap images should be disposed by Clear. + c.offscreen.Clear() + + setDrawingSkipped(i < updateCount-1) + + if err := hooks.RunBeforeUpdateHooks(); err != nil { + return err + } + if err := c.game.Update(c.offscreen); err != nil { + return err + } + uiDriver().Input().ResetForFrame() + afterFrameUpdate() } - if err := c.game.Update(c.offscreen); err != nil { - return err - } - uiDriver().Input().ResetForFrame() - afterFrameUpdate() } // c.screen might be nil when updateCount is 0 in the initial state (#1039).