diff --git a/doc.go b/doc.go index ab5ef315d..e5a4e16cd 100644 --- a/doc.go +++ b/doc.go @@ -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 { diff --git a/examples/windowsize/main.go b/examples/windowsize/main.go index b5d8ab31a..3b1ec74a0 100644 --- a/examples/windowsize/main.go +++ b/examples/windowsize/main.go @@ -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) + } } } diff --git a/internal/driver/ui.go b/internal/driver/ui.go index 6287cb5bd..03f2732c5 100644 --- a/internal/driver/ui.go +++ b/internal/driver/ui.go @@ -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. diff --git a/internal/uidriver/glfw/ui.go b/internal/uidriver/glfw/ui.go index edc8983b3..f715e28a0 100644 --- a/internal/uidriver/glfw/ui.go +++ b/internal/uidriver/glfw/ui.go @@ -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))) diff --git a/internal/uidriver/js/ui.go b/internal/uidriver/js/ui.go index be26e740a..3ac0ccfd8 100644 --- a/internal/uidriver/js/ui.go +++ b/internal/uidriver/js/ui.go @@ -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. diff --git a/internal/uidriver/mobile/ui.go b/internal/uidriver/mobile/ui.go index 22b29b993..34636003f 100644 --- a/internal/uidriver/mobile/ui.go +++ b/internal/uidriver/mobile/ui.go @@ -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 } diff --git a/run.go b/run.go index ef5060424..2bd2c7c63 100644 --- a/run.go +++ b/run.go @@ -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) { diff --git a/uicontext.go b/uicontext.go index 49e60c8cd..a2e6d04ec 100644 --- a/uicontext.go +++ b/uicontext.go @@ -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() diff --git a/window.go b/window.go index 7246657dc..e2205447e 100644 --- a/window.go +++ b/window.go @@ -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) +}