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).