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

18
doc.go
View File

@ -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) {

View File

@ -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,

View File

@ -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) {

View File

@ -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 {

View File

@ -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
}

60
run.go
View File

@ -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)
}

View File

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