ui: Add RunGame, WindowSize and SetWindowSize

This change introduces the new APIs RunGame, WindowSize and
SetWindowSize. These new APIs hides the notion of 'scale', and is
more flexible with the outside size change. This means that we can
introduce a resizable window.

This change also adds -legacy flag to examples/windowsize. If the
flag is off, the new APIs are used.

This change deprecates these functions since the notion of 'scale'
is deprecated:

  * ScreenScale
  * ScreenSizeInFullscreen
  * SetScreenScale
  * SetScreenSize

Fixes #943, #571
Updates #320
This commit is contained in:
Hajime Hoshi 2019-12-16 10:52:53 +09:00
parent 57d527bea2
commit 7d56e4335e
9 changed files with 255 additions and 104 deletions

38
doc.go
View File

@ -14,7 +14,43 @@
// Package ebiten provides graphics and input API to develop a 2D game.
//
// You can start the game by calling the function Run.
// You can start the game by calling the function RunGame.
//
// type Game struct{}
//
// // Update is called every frame (1/60 [s]).
// 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
// }
//
// // 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) {
// return 320, 240
// }
//
// func main() {
// // Sepcify the window size as you like. Here, a doulbed size is specified.
// ebiten.SetWindowSize(640, 480)
// ebiten.SetTitle("Your game's title")
// // Call ebiten.RunGame to start your game loop.
// if err := ebiten.Run(game); err != nil {
// log.Fatal(err)
// }
// }
//
// For backward compatibility, you can use a shorthand style Run.
//
// // update is called every frame (1/60 [s]).
// func update(screen *ebiten.Image) error {

View File

@ -37,6 +37,22 @@ import (
)
var (
// flagLegacy represents whether the legacy APIs are used or not.
// If flagLegacy is true, these legacy APIs are used:
//
// * ebiten.Run
// * ebiten.ScreenScale
// * ebiten.SetScreenScale
// * ebiten.SetScreenSize
//
// If flagLegacy is false, these APIs are used:
//
// * ebiten.RunGame
// * ebiten.SetWindowSize
// * ebiten.WindowSize
flagLegacy = flag.Bool("legacy", false, "use the legacy API")
flagFullscreen = flag.Bool("fullscreen", false, "fullscreen")
flagWindowPosition = flag.String("windowposition", "", "window position (e.g., 100,200)")
flagScreenTransparent = flag.Bool("screentransparent", false, "screen transparent")
)
@ -76,10 +92,32 @@ func createRandomIconImage() image.Image {
return img
}
func update(screen *ebiten.Image) error {
screenScale := ebiten.ScreenScale()
const d = 16
screenWidth, screenHeight := screen.Size()
type game struct {
width int
height int
}
func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) {
// Ignore the outside size. This means that the offscreen is not adjusted with the outside world.
return g.width, g.height
}
func (g *game) Update(screen *ebiten.Image) error {
var (
screenWidth int
screenHeight int
screenScale float64
)
if *flagLegacy {
screenWidth, screenHeight = screen.Size()
screenScale = ebiten.ScreenScale()
} else {
screenWidth = g.width
screenHeight = g.height
ww, _ := ebiten.WindowSize()
screenScale = float64(ww) / float64(g.width)
}
fullscreen := ebiten.IsFullscreen()
runnableInBackground := ebiten.IsRunnableInBackground()
cursorVisible := ebiten.IsCursorVisible()
@ -89,6 +127,7 @@ func update(screen *ebiten.Image) error {
positionX, positionY := ebiten.WindowPosition()
transparent := ebiten.IsScreenTransparent()
const d = 16
if ebiten.IsKeyPressed(ebiten.KeyShift) {
if inpututil.IsKeyJustPressed(ebiten.KeyUp) {
screenHeight += d
@ -164,9 +203,14 @@ func update(screen *ebiten.Image) error {
decorated = !decorated
}
ebiten.SetScreenSize(screenWidth, screenHeight)
// TODO: Add a flag for compatibility mode and call SetScreenScale only when the flag is on.
ebiten.SetScreenScale(screenScale)
if *flagLegacy {
ebiten.SetScreenSize(screenWidth, screenHeight)
ebiten.SetScreenScale(screenScale)
} else {
g.width = screenWidth
g.height = screenHeight
ebiten.SetWindowSize(int(float64(screenWidth)*screenScale), int(float64(screenHeight)*screenScale))
}
ebiten.SetFullscreen(fullscreen)
ebiten.SetRunnableInBackground(runnableInBackground)
ebiten.SetCursorVisible(cursorVisible)
@ -268,7 +312,27 @@ func main() {
}
ebiten.SetScreenTransparent(*flagScreenTransparent)
if err := ebiten.Run(update, initScreenWidth, initScreenHeight, initScreenScale, "Window Size (Ebiten Demo)"); err != nil {
log.Fatal(err)
g := &game{
width: initScreenWidth,
height: initScreenHeight,
}
if *flagFullscreen {
ebiten.SetFullscreen(true)
}
const title = "Window Size (Ebiten Demo)"
if *flagLegacy {
if err := ebiten.Run(g.Update, g.width, g.height, initScreenScale, title); err != nil {
log.Fatal(err)
}
} else {
w := int(float64(g.width) * initScreenScale)
h := int(float64(g.height) * initScreenScale)
ebiten.SetWindowSize(w, h)
ebiten.SetWindowTitle(title)
if err := ebiten.RunGame(g); err != nil {
log.Fatal(err)
}
}
}

View File

@ -43,6 +43,7 @@ type UI interface {
IsWindowResizable() bool
ScreenSizeInFullscreen() (int, int)
WindowPosition() (int, int)
WindowSize() (int, int)
IsScreenTransparent() bool
MonitorPosition() (int, int)
CanHaveWindow() bool // TODO: Create a 'Widnow' interface.

View File

@ -64,6 +64,8 @@ type UserInterface struct {
initWindowResizable bool
initWindowPositionXInDP int
initWindowPositionYInDP int
initWindowWidthInDP int
initWindowHeightInDP int
initScreenTransparent bool
initIconImages []image.Image
@ -80,17 +82,19 @@ type UserInterface struct {
const (
maxInt = int(^uint(0) >> 1)
minInt = -maxInt - 1
invalidPos = minInt
invalidVal = minInt
)
var (
theUI = &UserInterface{
origPosX: invalidPos,
origPosY: invalidPos,
origPosX: invalidVal,
origPosY: invalidVal,
initCursorMode: driver.CursorModeVisible,
initWindowDecorated: true,
initWindowPositionXInDP: invalidPos,
initWindowPositionYInDP: invalidPos,
initWindowPositionXInDP: invalidVal,
initWindowPositionYInDP: invalidVal,
initWindowWidthInDP: invalidVal,
initWindowHeightInDP: invalidVal,
vsync: true,
}
)
@ -302,10 +306,10 @@ func (u *UserInterface) setInitIconImages(iconImages []image.Image) {
func (u *UserInterface) getInitWindowPosition() (int, int) {
u.m.RLock()
defer u.m.RUnlock()
if u.initWindowPositionXInDP != invalidPos && u.initWindowPositionYInDP != invalidPos {
if u.initWindowPositionXInDP != invalidVal && u.initWindowPositionYInDP != invalidVal {
return u.initWindowPositionXInDP, u.initWindowPositionYInDP
}
return invalidPos, invalidPos
return invalidVal, invalidVal
}
func (u *UserInterface) setInitWindowPosition(x, y int) {
@ -316,6 +320,19 @@ func (u *UserInterface) setInitWindowPosition(x, y int) {
u.initWindowPositionYInDP = y
}
func (u *UserInterface) getInitWindowSize() (int, int) {
u.m.Lock()
w, h := u.initWindowWidthInDP, u.initWindowHeightInDP
u.m.Unlock()
return w, h
}
func (u *UserInterface) setInitWindowSize(width, height int) {
u.m.Lock()
u.initWindowWidthInDP, u.initWindowHeightInDP = width, height
u.m.Unlock()
}
// toDeviceIndependentPixel must be called from the main thread.
func (u *UserInterface) toDeviceIndependentPixel(x float64) float64 {
return x / u.glfwScale()
@ -695,12 +712,16 @@ func (u *UserInterface) run(context driver.UIContext) error {
}
u.SetWindowPosition(u.getInitWindowPosition())
if w, h := u.getInitWindowSize(); w != invalidVal && h != invalidVal {
w = int(u.toDeviceDependentPixel(float64(w)))
h = int(u.toDeviceDependentPixel(float64(h)))
u.setWindowSize(w, h, u.isFullscreen(), u.vsync)
}
_ = u.t.Call(func() error {
u.title = u.getInitTitle()
u.window.SetTitle(u.title)
u.window.Show()
return nil
})
@ -892,7 +913,7 @@ func (u *UserInterface) setWindowSize(width, height int, fullscreen bool, vsync
u.swapBuffers()
if fullscreen {
if u.origPosX == invalidPos || u.origPosY == invalidPos {
if u.origPosX == invalidVal || u.origPosY == invalidVal {
u.origPosX, u.origPosY = u.window.GetPos()
}
m := u.currentMonitor()
@ -941,7 +962,7 @@ func (u *UserInterface) setWindowSize(width, height int, fullscreen bool, vsync
width = minWindowWidth
}
if u.origPosX != invalidPos && u.origPosY != invalidPos {
if u.origPosX != invalidVal && u.origPosY != invalidVal {
x := u.origPosX
y := u.origPosY
u.window.SetPos(x, y)
@ -951,8 +972,8 @@ func (u *UserInterface) setWindowSize(width, height int, fullscreen bool, vsync
u.window.SetPos(x+1, y)
u.window.SetPos(x, y)
}
u.origPosX = invalidPos
u.origPosY = invalidPos
u.origPosX = invalidVal
u.origPosY = invalidVal
}
// Set the window size after the position. The order matters.
@ -1083,9 +1104,19 @@ func (u *UserInterface) IsScreenTransparent() bool {
return val
}
func (u *UserInterface) WindowSize() (int, int) {
if !u.isRunning() {
return u.getInitWindowSize()
}
w := int(u.toDeviceIndependentPixel(float64(u.windowWidth)))
h := int(u.toDeviceIndependentPixel(float64(u.windowHeight)))
return w, h
}
func (u *UserInterface) SetWindowSize(width, height int) {
if !u.isRunning() {
panic("glfw: SetWindowSize can't be called before the main loop starts")
u.setInitWindowSize(width, height)
return
}
w := int(u.toDeviceDependentPixel(float64(width)))
h := int(u.toDeviceDependentPixel(float64(height)))

View File

@ -142,6 +142,10 @@ func (u *UserInterface) SetWindowResizable(decorated bool) {
// Do nothing
}
func (u *UserInterface) WindowSize() (int, int) {
return 0, 0
}
func (u *UserInterface) SetWindowSize(width, height int) {
// TODO: This is too tricky: Even though browsers don't have windows, SetWindowSize is called whenever the
// screen size is changed. Fix this hack.

View File

@ -433,6 +433,10 @@ func (u *UserInterface) SetWindowSize(width, height int) {
// Do nothing
}
func (u *UserInterface) WindowSize() (int, int) {
return 0, 0
}
func (u *UserInterface) CanHaveWindow() bool {
return false
}

141
run.go
View File

@ -24,14 +24,12 @@ import (
var _ = __EBITEN_REQUIRES_GO_VERSION_1_12_OR_LATER__
// Game defines necessary functions for a game.
//
// Note: This interface is not used anywhere yet.
type Game interface {
// Update updates a game by one frame.
Update(*Image) error
// Layout accepts a native outside size in DP (device-independent pixels) and returns the game's logical
// screen size.
// Layout accepts a native outside size in device-independent pixels and returns the game's logical screen
// size.
//
// The screen scale is automatically adjusted to fit the outside.
//
@ -104,11 +102,14 @@ func IsRunningSlowly() bool {
return IsDrawingSkipped()
}
// Run runs the game.
// Run starts the main loop and runs the game.
// f is a function which is called at every frame.
// The argument (*Image) is the render target that represents the screen.
// The screen size is based on the given values (width and height).
//
// Run is a shorthand for RunGame, but there are some restrictions.
// If you want to resize the window by dragging, use RunGame instead.
//
// A window size is based on the given values (width, height and scale).
//
// scale is used to enlarge the screen on desktops.
@ -156,7 +157,9 @@ func Run(f func(*Image) error, width, height int, scale float64, title string) e
}
theUIContext = newUIContext(game, scale)
fixWindowPosition(int(float64(width)*scale), int(float64(height)*scale))
ww, wh := int(float64(width)*scale), int(float64(height)*scale)
fixWindowPosition(ww, wh)
SetWindowSize(ww, wh)
SetWindowTitle(title)
if err := uiDriver().Run(theUIContext, graphicsDriver()); err != nil {
if err == driver.RegularTermination {
@ -168,6 +171,58 @@ func Run(f func(*Image) error, width, height int, scale float64, title string) e
return nil
}
// RunGame starts the main loop and runs the game.
// game's Update function is called every frame.
// game's Layout function is called when necessary, and you can specify the logical screen size in the function.
//
// RunGame is a more flexibile form of Run due to 'Layout' function.
// The window is resizable if you use RunGame, while you cannot if you use Run.
// RunGame is more sophisticated way than Run and hides the notion of 'scale'.
//
// On desktops, SetWindowSize must be called before RunGame is called.
//
// A window size is based on the given values (width, height and scale).
//
// RunGame must be called on the main thread.
// Note that Ebiten bounds the main goroutine to the main OS thread by runtime.LockOSThread.
//
// Ebiten tries to call game's Update function 60 times a second by default. In other words,
// TPS (ticks per second) is 60 by default.
// This is not related to framerate (display's refresh rate).
//
// game's Update is not called when the window is in background by default.
// This setting is configurable with SetRunnableInBackground.
//
// The given scale is ignored on fullscreen mode or gomobile-build mode.
//
// On non-GopherJS environments, RunGame returns error when 1) OpenGL error happens, 2) audio error happens or
// 3) f returns error. In the case of 3), RunGame returns the same error.
//
// On GopherJS, RunGame returns immediately.
// It is because the 'main' goroutine cannot be blocked on GopherJS due to the bug (gopherjs/gopherjs#826).
// When an error happens, this is shown as an error on the console.
//
// The size unit is device-independent pixel.
//
// Don't call RunGame twice or more in one process.
func RunGame(game Game) error {
if uiDriver().CanHaveWindow() {
w, h := WindowSize()
if w < 0 || h < 0 {
panic("ebiten: SetWindowSize must be called before RunGame on desktops")
}
fixWindowPosition(w, h)
}
theUIContext = newUIContext(game, 1)
if err := uiDriver().Run(theUIContext, graphicsDriver()); err != nil {
if err == driver.RegularTermination {
return nil
}
return err
}
return nil
}
// RunWithoutMainLoop runs the game, but don't call the loop on the main (UI) thread.
// Different from Run, RunWithoutMainLoop returns immediately.
//
@ -184,49 +239,17 @@ func RunWithoutMainLoop(f func(*Image) error, width, height int, scale float64,
return uiDriver().RunWithoutMainLoop(width, height, scale, title, theUIContext, graphicsDriver())
}
// ScreenSizeInFullscreen returns the size in device-independent pixels when the game is fullscreen.
// The adopted monitor is the 'current' monitor which the window belongs to.
// The returned value can be given to Run or SetSize function if the perfectly fit fullscreen is needed.
//
// On browsers, ScreenSizeInFullscreen returns the 'window' (global object) size, not 'screen' size since an Ebiten game
// should not know the outside of the window object.
// For more details, see SetFullscreen API comment.
//
// On mobiles, ScreenSizeInFullscreen returns (0, 0) so far.
//
// If you use this for screen size with SetFullscreen(true), you can get the fullscreen mode
// which size is well adjusted with the monitor.
//
// w, h := ScreenSizeInFullscreen()
// ebiten.SetFullscreen(true)
// ebiten.Run(update, w, h, 1, "title")
//
// Furthermore, you can use them with DeviceScaleFactor(), you can get the finest
// fullscreen mode.
//
// s := ebiten.DeviceScaleFactor()
// w, h := ScreenSizeInFullscreen()
// ebiten.SetFullscreen(true)
// ebiten.Run(update, int(float64(w) * s), int(float64(h) * s), 1/s, "title")
//
// For actual example, see examples/fullscreen
//
// ScreenSizeInFullscreen must be called on the main thread before ebiten.Run, and is concurrent-safe after ebiten.Run.
// ScreenSizeInFullscreen is deprecated as of 1.11.0-alpha.
func ScreenSizeInFullscreen() (int, int) {
return uiDriver().ScreenSizeInFullscreen()
}
// MonitorSize is deprecated as of 1.8.0-alpha. Use ScreenSizeInFullscreen instead.
// MonitorSize is deprecated as of 1.8.0-alpha.
func MonitorSize() (int, int) {
return ScreenSizeInFullscreen()
}
// SetScreenSize changes the (logical) size of the screen.
// SetScreenSize adjusts the window size on desktops without changing its scale.
//
// The unit is device-independent pixel.
//
// SetScreenSize is concurrent-safe.
// SetScreenSize is deprecated as of 1.11.0-alpha. Use SetWindowSize and RunGame (Game's Layout) instead.
func SetScreenSize(width, height int) {
if width <= 0 || height <= 0 {
panic("ebiten: width and height must be positive")
@ -237,27 +260,7 @@ func SetScreenSize(width, height int) {
theUIContext.SetScreenSize(width, height)
}
// SetScreenScale changes the scale of the screen on desktops.
//
// Note that the actual screen is multiplied not only by the given scale but also
// by the device scale on high-DPI display.
// If you pass inverse of the device scale,
// you can disable this automatical device scaling as a result.
// You can get the device scale by DeviceScaleFactor function.
//
// On browsers, SetScreenScale saves the given value and affects the returned value of ScreenScale,
// but does not affect actual rendering.
// SetScreenScale works as this as of 1.10.0-alpha.
// Before that, SetScreenScale affected the rendering scale.
//
// On mobiles, SetScreenScale works, but usually the user doesn't have to call this.
// Instead, ebitenmobile calls this automatically.
//
// SetScreenScale panics if scale is not a positive number.
//
// SetScreenScale is concurrent-safe.
//
// TODO: Deprecate this function.
// SetScreenScale is deprecated as of 1.11.0-alpha. Use SetWindowSize instead.
func SetScreenScale(scale float64) {
if scale <= 0 {
panic("ebiten: scale must be positive")
@ -268,15 +271,7 @@ func SetScreenScale(scale float64) {
theUIContext.setScaleForWindow(scale)
}
// ScreenScale returns the current screen scale.
//
// On browsers, this value does not affect actual rendering.
//
// If Run is not called, this returns 0.
//
// ScreenScale is concurrent-safe.
//
// TODO: Deprecate this function.
// ScreenScale is deprecated as of 1.11.0-alpha. Use WindowSize instead.
func ScreenScale() float64 {
if theUIContext == nil {
panic("ebiten: ScreenScale can't be called before the main loop starts")
@ -391,7 +386,7 @@ func SetRunnableInBackground(runnableInBackground bool) {
// DeviceScaleFactor might panic on init function on some devices like Android.
// Then, it is not recommended to call DeviceScaleFactor from init functions.
//
// DeviceScaleFactor must be called on the main thread before ebiten.Run, and is concurrent-safe after ebiten.Run.
// DeviceScaleFactor must be called on the main thread before the main loop, and is concurrent-safe after the main loop.
func DeviceScaleFactor() float64 {
return uiDriver().DeviceScaleFactor()
}
@ -462,7 +457,7 @@ func IsScreenTransparent() bool {
// SetScreenTransparent sets the state if the window is transparent.
//
// SetScreenTransparent panics if SetScreenTransparent is called after Run.
// SetScreenTransparent panics if SetScreenTransparent is called after the main loop.
//
// SetScreenTransparent does nothing on mobiles.
func SetScreenTransparent(transparent bool) {

View File

@ -86,7 +86,7 @@ var theUIContext *uiContext
func (c *uiContext) setScaleForWindow(scale float64) {
g, ok := c.game.(*defaultGame)
if !ok {
panic("ebiten: setScaleForWindow must be called when Run is used")
panic("ebiten: setScaleForWindow can be called only when Run is used")
}
c.m.Lock()
@ -99,7 +99,7 @@ func (c *uiContext) setScaleForWindow(scale float64) {
func (c *uiContext) getScaleForWindow() float64 {
if _, ok := c.game.(*defaultGame); !ok {
panic("ebiten: getScaleForWindow must be called when Run is used")
panic("ebiten: getScaleForWindow can be called only when Run is used")
}
c.m.Lock()
@ -115,7 +115,7 @@ func (c *uiContext) getScaleForWindow() float64 {
func (c *uiContext) SetScreenSize(width, height int) {
g, ok := c.game.(*defaultGame)
if !ok {
panic("ebiten: SetScreenSize must be called when Run is used")
panic("ebiten: SetScreenSize can be called only when Run is used")
}
c.m.Lock()
@ -157,7 +157,11 @@ func (c *uiContext) updateOffscreen() {
if c.offscreen == nil {
c.offscreen = newImage(sw, sh, FilterDefault, true)
}
c.SetScreenSize(sw, sh)
// The window size is automatically adjusted when Run is used.
if _, ok := c.game.(*defaultGame); ok {
c.SetScreenSize(sw, sh)
}
// TODO: This is duplicated with mobile/ebitenmobileview/funcs.go. Refactor this.
d := uiDriver().DeviceScaleFactor()

View File

@ -55,7 +55,7 @@ func IsWindowDecorated() bool {
// setWindowResizable works only on desktops.
// setWindowResizable does nothing on other platforms.
//
// setWindowResizable panics if setWindowResizable is called after Run.
// setWindowResizable panics if setWindowResizable is called after the main loop.
//
// setWindowResizable is concurrent-safe.
func setWindowResizable(resizable bool) {
@ -104,7 +104,7 @@ func SetWindowIcon(iconImages []image.Image) {
// WindowPosition returns the window position.
//
// WindowPosition panics before Run is called.
// WindowPosition panics if the main loop does not start yet.
//
// WindowPosition returns the last window position on fullscreen mode.
//
@ -120,8 +120,6 @@ func WindowPosition() (x, y int) {
// SetWindowPosition sets the window position.
//
// SetWindowPosition works before and after Run is called.
//
// SetWindowPosition does nothing on fullscreen mode.
//
// SetWindowPosition does nothing on browsers and mobiles.
@ -185,3 +183,17 @@ func fixWindowPosition(width, height int) {
uiDriver().SetWindowPosition(initWindowPositionX, initWindowPositionY)
}
}
// WindowSize returns the window size. On fullscreen mode, WindowSize returns the original window size.
//
// WindowSize is concurrent-safe.
func WindowSize() (int, int) {
return uiDriver().WindowSize()
}
// SetWindowSize sets the window size. On fullscreen mode, SetWindowSize sets the original window size.
//
// SetWindowSize is concurrent-safe.
func SetWindowSize(width, height int) {
uiDriver().SetWindowSize(width, height)
}