Skip to content

Commit

Permalink
ui: Add an optional function Draw function to Game interface (hajimeh…
Browse files Browse the repository at this point in the history
…oshi#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 hajimehoshi#1104
  • Loading branch information
hajimehoshi authored Mar 24, 2020
1 parent c5a7d5d commit 237498e
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 39 deletions.
16 changes: 6 additions & 10 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,16 @@
// 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.
// return nil
// }
//
// if ebiten.IsDrawingSkipped() {
// // When the game is running slowly, the rendering result
// // will not be adopted.
// 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.
//
// return nil
// }
//
// // Layout takes the outside size (e.g., the window size) and returns the (logical) screen size.
Expand Down
8 changes: 3 additions & 5 deletions examples/moire/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,11 @@ func (g *game) Update(screen *ebiten.Image) error {
fullscreen = !fullscreen
ebiten.SetFullscreen(fullscreen)
}
return nil
}

if ebiten.IsDrawingSkipped() {
return nil
}

func (g *game) Draw(screen *ebiten.Image) {
screen.ReplacePixels(getDots(screen.Size()))
return nil
}

func main() {
Expand Down
17 changes: 8 additions & 9 deletions examples/windowsize/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions imagedumper_desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions imagedumper_notdesktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 56 additions & 4 deletions run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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'.
Expand Down Expand Up @@ -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)
}

Expand Down
46 changes: 35 additions & 11 deletions uicontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

if err := hooks.RunBeforeUpdateHooks(); err != nil {
return err
}
if err := c.game.Update(c.offscreen); err != nil {
return err
game.Draw(c.offscreen)
} else {
for i := 0; i < updateCount; i++ {
c.updateOffscreen()

// 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()
}
uiDriver().Input().ResetForFrame()
afterFrameUpdate()
}

// c.screen might be nil when updateCount is 0 in the initial state (#1039).
Expand Down

0 comments on commit 237498e

Please sign in to comment.