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
This commit is contained in:
Hajime Hoshi 2020-03-24 12:01:37 +09:00 committed by GitHub
parent c5a7d5d6a9
commit 237498e51f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 118 additions and 40 deletions

12
doc.go
View File

@ -20,20 +20,16 @@
// type Game struct{} // type Game struct{}
// //
// // Update proceeds the game state. // // 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 { // func (g *Game) Update(screen *ebiten.Image) error {
//
// // Write your game's logical update. // // Write your game's logical update.
//
// if ebiten.IsDrawingSkipped() {
// // When the game is running slowly, the rendering result
// // will not be adopted.
// return nil // 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. // // Write your game's rendering.
//
// return nil
// } // }
// //
// // Layout takes the outside size (e.g., the window size) and returns the (logical) screen size. // // Layout takes the outside size (e.g., the window size) and returns the (logical) screen size.

View File

@ -86,13 +86,11 @@ func (g *game) Update(screen *ebiten.Image) error {
fullscreen = !fullscreen fullscreen = !fullscreen
ebiten.SetFullscreen(fullscreen) ebiten.SetFullscreen(fullscreen)
} }
if ebiten.IsDrawingSkipped() {
return nil return nil
} }
func (g *game) Draw(screen *ebiten.Image) {
screen.ReplacePixels(getDots(screen.Size())) screen.ReplacePixels(getDots(screen.Size()))
return nil
} }
func main() { func main() {

View File

@ -101,6 +101,7 @@ func createRandomIconImage() image.Image {
type game struct { type game struct {
width int width int
height int height int
transparent bool
} }
func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) { func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) {
@ -139,7 +140,7 @@ func (g *game) Update(screen *ebiten.Image) error {
tps := ebiten.MaxTPS() tps := ebiten.MaxTPS()
decorated := ebiten.IsWindowDecorated() decorated := ebiten.IsWindowDecorated()
positionX, positionY := ebiten.WindowPosition() positionX, positionY := ebiten.WindowPosition()
transparent := ebiten.IsScreenTransparent() g.transparent = ebiten.IsScreenTransparent()
floating := ebiten.IsWindowFloating() floating := ebiten.IsWindowFloating()
resizable := ebiten.IsWindowResizable() resizable := ebiten.IsWindowResizable()
@ -269,12 +270,11 @@ func (g *game) Update(screen *ebiten.Image) error {
} }
count++ count++
if ebiten.IsDrawingSkipped() {
return nil return nil
} }
if !transparent { func (g *game) Draw(screen *ebiten.Image) {
if !g.transparent {
screen.Fill(color.RGBA{0x80, 0x80, 0xc0, 0xff}) screen.Fill(color.RGBA{0x80, 0x80, 0xc0, 0xff})
} }
w, h := gophersImage.Size() w, h := gophersImage.Size()
@ -336,7 +336,6 @@ TPS: Current: %0.2f / Max: %s
FPS: %0.2f FPS: %0.2f
Device Scale Factor: %0.2f`, msgS, msgM, msgR, fg, wx, wy, cx, cy, ebiten.CurrentTPS(), tpsStr, ebiten.CurrentFPS(), ebiten.DeviceScaleFactor()) Device Scale Factor: %0.2f`, msgS, msgM, msgR, fg, wx, wy, cx, cy, ebiten.CurrentTPS(), tpsStr, ebiten.CurrentFPS(), ebiten.DeviceScaleFactor())
ebitenutil.DebugPrint(screen, msg) ebitenutil.DebugPrint(screen, msg)
return nil
} }
func parseWindowPosition() (int, int, bool) { func parseWindowPosition() (int, int, bool) {

View File

@ -158,6 +158,10 @@ func (i *imageDumper) update(screen *Image) error {
return nil return nil
} }
return i.dump(screen)
}
func (i *imageDumper) dump(screen *Image) error {
if i.toTakeScreenshot { if i.toTakeScreenshot {
i.toTakeScreenshot = false i.toTakeScreenshot = false
if err := takeScreenshot(screen); err != nil { if err := takeScreenshot(screen); err != nil {

View File

@ -23,3 +23,8 @@ type imageDumper struct {
func (i *imageDumper) update(screen *Image) error { func (i *imageDumper) update(screen *Image) error {
return i.f(screen) return i.f(screen)
} }
func (i *imageDumper) dump(screen *Image) error {
// Do nothing
return nil
}

60
run.go
View File

@ -23,9 +23,22 @@ import (
// Game defines necessary functions for a game. // Game defines necessary functions for a game.
type Game interface { 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 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 // Layout accepts a native outside size in device-independent pixels and returns the game's logical screen
// size. // size.
// //
@ -94,13 +107,16 @@ func setDrawingSkipped(skipped bool) {
// return nil // 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. // IsDrawingSkipped is concurrent-safe.
func IsDrawingSkipped() bool { func IsDrawingSkipped() bool {
return atomic.LoadInt32(&isDrawingSkipped) != 0 return atomic.LoadInt32(&isDrawingSkipped) != 0
} }
// IsRunningSlowly is deprecated as of 1.8.0-alpha. // IsRunningSlowly is deprecated as of 1.8.0-alpha. Use Game's Draw function instead.
// Use IsDrawingSkipped instead.
func IsRunningSlowly() bool { func IsRunningSlowly() bool {
return IsDrawingSkipped() return IsDrawingSkipped()
} }
@ -184,10 +200,41 @@ func (i *imageDumperGame) Layout(outsideWidth, outsideHeight int) (screenWidth,
return i.game.Layout(outsideWidth, outsideHeight) 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. // 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'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. // 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. // 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'. // 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. // Don't call RunGame twice or more in one process.
func RunGame(game Game) error { func RunGame(game Game) error {
fixWindowPosition(WindowSize()) fixWindowPosition(WindowSize())
if _, ok := game.(interface{ Draw(*Image) }); ok {
return runGame(&imageDumperGameWithDraw{
imageDumperGame: imageDumperGame{game: game},
}, 0)
}
return runGame(&imageDumperGame{game: game}, 0) return runGame(&imageDumperGame{game: game}, 0)
} }

View File

@ -248,6 +248,29 @@ func (c *uiContext) Update(afterFrameUpdate func()) error {
func (c *uiContext) update(afterFrameUpdate func()) error { func (c *uiContext) update(afterFrameUpdate func()) error {
updateCount := clock.Update(MaxTPS()) updateCount := clock.Update(MaxTPS())
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()
game.Draw(c.offscreen)
} else {
for i := 0; i < updateCount; i++ { for i := 0; i < updateCount; i++ {
c.updateOffscreen() c.updateOffscreen()
@ -265,6 +288,7 @@ func (c *uiContext) update(afterFrameUpdate func()) error {
uiDriver().Input().ResetForFrame() uiDriver().Input().ResetForFrame()
afterFrameUpdate() afterFrameUpdate()
} }
}
// c.screen might be nil when updateCount is 0 in the initial state (#1039). // c.screen might be nil when updateCount is 0 in the initial state (#1039).
if c.screen == nil { if c.screen == nil {