mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-25 11:18:54 +01:00
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:
parent
c5a7d5d6a9
commit
237498e51f
18
doc.go
18
doc.go
@ -20,22 +20,18 @@
|
|||||||
// 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
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Write your game's rendering.
|
|
||||||
//
|
|
||||||
// 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.
|
||||||
|
// }
|
||||||
|
//
|
||||||
// // 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.
|
||||||
// // If you don't have to adjust the screen size with the outside size, just return a fixed 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) {
|
// func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
|
||||||
|
@ -86,15 +86,13 @@ func (g *game) Update(screen *ebiten.Image) error {
|
|||||||
fullscreen = !fullscreen
|
fullscreen = !fullscreen
|
||||||
ebiten.SetFullscreen(fullscreen)
|
ebiten.SetFullscreen(fullscreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ebiten.IsDrawingSkipped() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
screen.ReplacePixels(getDots(screen.Size()))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *game) Draw(screen *ebiten.Image) {
|
||||||
|
screen.ReplacePixels(getDots(screen.Size()))
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
g := &game{
|
g := &game{
|
||||||
scale: initScreenScale,
|
scale: initScreenScale,
|
||||||
|
@ -99,8 +99,9 @@ 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++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if ebiten.IsDrawingSkipped() {
|
func (g *game) Draw(screen *ebiten.Image) {
|
||||||
return nil
|
if !g.transparent {
|
||||||
}
|
|
||||||
|
|
||||||
if !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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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
60
run.go
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
44
uicontext.go
44
uicontext.go
@ -248,22 +248,46 @@ 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())
|
||||||
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.
|
// Mipmap images should be disposed by Clear.
|
||||||
c.offscreen.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 {
|
// Mipmap images should be disposed by Clear.
|
||||||
return err
|
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).
|
// c.screen might be nil when updateCount is 0 in the initial state (#1039).
|
||||||
|
Loading…
Reference in New Issue
Block a user