1
0

main.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. // Copyright 2015 Hajime Hoshi
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package main
  15. import (
  16. "bytes"
  17. "flag"
  18. "fmt"
  19. "image"
  20. _ "image/jpeg"
  21. "log"
  22. "math"
  23. "math/rand/v2"
  24. "regexp"
  25. "strconv"
  26. "strings"
  27. "sync"
  28. "github.com/hajimehoshi/ebiten/v2"
  29. "github.com/hajimehoshi/ebiten/v2/ebitenutil"
  30. "github.com/hajimehoshi/ebiten/v2/examples/resources/images"
  31. "github.com/hajimehoshi/ebiten/v2/inpututil"
  32. )
  33. var (
  34. flagFullscreen = flag.Bool("fullscreen", false, "fullscreen")
  35. flagResizable = flag.Bool("resizable", false, "make the window resizable")
  36. flagWindowPosition = flag.String("windowposition", "", "window position (e.g., 100,200)")
  37. flagTransparent = flag.Bool("transparent", false, "screen transparent")
  38. flagAutoAdjusting = flag.Bool("autoadjusting", false, "make the game screen auto-adjusting")
  39. flagFloating = flag.Bool("floating", false, "make the window floating")
  40. flagMaximize = flag.Bool("maximize", false, "maximize the window")
  41. flagVsync = flag.Bool("vsync", true, "enable vsync")
  42. flagAutoRestore = flag.Bool("autorestore", false, "restore the window automatically")
  43. flagInitFocused = flag.Bool("initfocused", true, "whether the window is focused on start")
  44. flagMinWindowSize = flag.String("minwindowsize", "", "minimum window size (e.g., 100x200)")
  45. flagMaxWindowSize = flag.String("maxwindowsize", "", "maximum window size (e.g., 1920x1080)")
  46. flagGraphicsLibrary = flag.String("graphicslibrary", "", "graphics library (e.g. opengl)")
  47. flagRunnableOnUnfocused = flag.Bool("runnableonunfocused", true, "whether the app is runnable even on unfocused")
  48. flagColorSpace = flag.String("colorspace", "", "color space ('', 'srgb', or 'display-p3')")
  49. )
  50. func init() {
  51. flag.Parse()
  52. }
  53. const (
  54. initScreenWidth = 480
  55. initScreenHeight = 480
  56. initScreenScale = 1
  57. )
  58. var (
  59. gophersImage *ebiten.Image
  60. )
  61. func createRandomIconImage() image.Image {
  62. const size = 32
  63. rf := float64(rand.IntN(0x100))
  64. gf := float64(rand.IntN(0x100))
  65. bf := float64(rand.IntN(0x100))
  66. img := ebiten.NewImage(size, size)
  67. pix := make([]byte, 4*size*size)
  68. for j := 0; j < size; j++ {
  69. for i := 0; i < size; i++ {
  70. af := float64(i+j) / float64(2*size)
  71. if af > 0 {
  72. pix[4*(j*size+i)] = byte(rf * af)
  73. pix[4*(j*size+i)+1] = byte(gf * af)
  74. pix[4*(j*size+i)+2] = byte(bf * af)
  75. pix[4*(j*size+i)+3] = byte(af * 0xff)
  76. }
  77. }
  78. }
  79. img.WritePixels(pix)
  80. return img
  81. }
  82. type game struct {
  83. count int
  84. width float64
  85. height float64
  86. transparent bool
  87. logOnce sync.Once
  88. }
  89. func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) {
  90. // As game implements the interface LayoutFer, Layout is never called and LayoutF is called instead.
  91. // However, game has to implement Layout to satisfy the interface Game.
  92. panic("windowsize: Layout must not be called")
  93. }
  94. func (g *game) LayoutF(outsideWidth, outsideHeight float64) (float64, float64) {
  95. if *flagAutoAdjusting {
  96. g.width, g.height = outsideWidth, outsideHeight
  97. return outsideWidth, outsideHeight
  98. }
  99. // Ignore the outside size. This means that the offscreen is not adjusted with the outside world.
  100. return g.width, g.height
  101. }
  102. func (g *game) Update() error {
  103. g.logOnce.Do(func() {
  104. var debug ebiten.DebugInfo
  105. ebiten.ReadDebugInfo(&debug)
  106. fmt.Printf("Graphics library: %s\n", debug.GraphicsLibrary)
  107. })
  108. var (
  109. screenWidth float64
  110. screenHeight float64
  111. screenScale float64
  112. )
  113. screenWidth = g.width
  114. screenHeight = g.height
  115. if ww, wh := ebiten.WindowSize(); ww > 0 && wh > 0 {
  116. screenScale = math.Min(float64(ww)/g.width, float64(wh)/g.height)
  117. } else {
  118. // ebiten.WindowSize can return (0, 0) on browsers or mobiles.
  119. screenScale = 1
  120. }
  121. fullscreen := ebiten.IsFullscreen()
  122. runnableOnUnfocused := ebiten.IsRunnableOnUnfocused()
  123. cursorMode := ebiten.CursorMode()
  124. vsyncEnabled := ebiten.IsVsyncEnabled()
  125. tps := ebiten.TPS()
  126. decorated := ebiten.IsWindowDecorated()
  127. positionX, positionY := ebiten.WindowPosition()
  128. g.transparent = ebiten.IsScreenTransparent()
  129. floating := ebiten.IsWindowFloating()
  130. resizingMode := ebiten.WindowResizingMode()
  131. screenCleared := ebiten.IsScreenClearedEveryFrame()
  132. mousePassthrough := ebiten.IsWindowMousePassthrough()
  133. const d = 16
  134. toUpdateWindowSize := false
  135. toUpdateWindowPosition := false
  136. if ebiten.IsKeyPressed(ebiten.KeyShift) {
  137. if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) {
  138. screenHeight += d
  139. toUpdateWindowSize = true
  140. }
  141. if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) {
  142. if 16 < screenHeight && d < screenHeight {
  143. screenHeight -= d
  144. toUpdateWindowSize = true
  145. }
  146. }
  147. if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) {
  148. if 16 < screenWidth && d < screenWidth {
  149. screenWidth -= d
  150. toUpdateWindowSize = true
  151. }
  152. }
  153. if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) {
  154. screenWidth += d
  155. toUpdateWindowSize = true
  156. }
  157. } else {
  158. if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) {
  159. positionY -= d
  160. toUpdateWindowPosition = true
  161. }
  162. if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) {
  163. positionY += d
  164. toUpdateWindowPosition = true
  165. }
  166. if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) {
  167. positionX -= d
  168. toUpdateWindowPosition = true
  169. }
  170. if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) {
  171. positionX += d
  172. toUpdateWindowPosition = true
  173. }
  174. }
  175. if inpututil.IsKeyJustPressed(ebiten.KeyS) && !*flagAutoAdjusting {
  176. switch {
  177. case screenScale < 1:
  178. screenScale = 1
  179. case screenScale < 1.5:
  180. screenScale = 1.5
  181. case screenScale < 2:
  182. screenScale = 2
  183. default:
  184. screenScale = 0.75
  185. }
  186. toUpdateWindowSize = true
  187. }
  188. if inpututil.IsKeyJustPressed(ebiten.KeyF) {
  189. fullscreen = !fullscreen
  190. }
  191. if inpututil.IsKeyJustPressed(ebiten.KeyU) {
  192. runnableOnUnfocused = !runnableOnUnfocused
  193. }
  194. if inpututil.IsKeyJustPressed(ebiten.KeyC) {
  195. switch cursorMode {
  196. case ebiten.CursorModeVisible:
  197. cursorMode = ebiten.CursorModeHidden
  198. case ebiten.CursorModeHidden:
  199. cursorMode = ebiten.CursorModeCaptured
  200. case ebiten.CursorModeCaptured:
  201. cursorMode = ebiten.CursorModeVisible
  202. }
  203. }
  204. if inpututil.IsKeyJustPressed(ebiten.KeyV) {
  205. vsyncEnabled = !vsyncEnabled
  206. }
  207. if inpututil.IsKeyJustPressed(ebiten.KeyT) {
  208. switch tps {
  209. case ebiten.SyncWithFPS:
  210. tps = 30
  211. case 30:
  212. tps = 60
  213. case 60:
  214. tps = 120
  215. case 120:
  216. tps = ebiten.SyncWithFPS
  217. default:
  218. panic("not reached")
  219. }
  220. }
  221. if inpututil.IsKeyJustPressed(ebiten.KeyD) {
  222. decorated = !decorated
  223. }
  224. if inpututil.IsKeyJustPressed(ebiten.KeyL) {
  225. floating = !floating
  226. }
  227. if inpututil.IsKeyJustPressed(ebiten.KeyR) {
  228. switch resizingMode {
  229. case ebiten.WindowResizingModeDisabled:
  230. resizingMode = ebiten.WindowResizingModeOnlyFullscreenEnabled
  231. case ebiten.WindowResizingModeOnlyFullscreenEnabled:
  232. resizingMode = ebiten.WindowResizingModeEnabled
  233. case ebiten.WindowResizingModeEnabled:
  234. resizingMode = ebiten.WindowResizingModeDisabled
  235. default:
  236. panic("not reached")
  237. }
  238. }
  239. if inpututil.IsKeyJustPressed(ebiten.KeyW) {
  240. screenCleared = !screenCleared
  241. }
  242. maximize := inpututil.IsKeyJustPressed(ebiten.KeyM)
  243. minimize := inpututil.IsKeyJustPressed(ebiten.KeyN)
  244. restore := false
  245. if ebiten.IsWindowMaximized() || ebiten.IsWindowMinimized() {
  246. if *flagAutoRestore {
  247. restore = g.count%ebiten.TPS() == 0
  248. } else {
  249. restore = inpututil.IsKeyJustPressed(ebiten.KeyE)
  250. }
  251. }
  252. if inpututil.IsKeyJustPressed(ebiten.KeyP) {
  253. mousePassthrough = !mousePassthrough
  254. }
  255. if toUpdateWindowSize {
  256. g.width = screenWidth
  257. g.height = screenHeight
  258. ebiten.SetWindowSize(int(float64(screenWidth)*screenScale), int(float64(screenHeight)*screenScale))
  259. }
  260. ebiten.SetFullscreen(fullscreen)
  261. ebiten.SetRunnableOnUnfocused(runnableOnUnfocused)
  262. ebiten.SetCursorMode(cursorMode)
  263. // Set FPS mode enabled only when this is needed.
  264. // This makes a bug around FPS mode initialization more explicit (#1364).
  265. if vsyncEnabled != ebiten.IsVsyncEnabled() {
  266. ebiten.SetVsyncEnabled(vsyncEnabled)
  267. }
  268. ebiten.SetTPS(tps)
  269. ebiten.SetWindowDecorated(decorated)
  270. if toUpdateWindowPosition {
  271. ebiten.SetWindowPosition(positionX, positionY)
  272. }
  273. ebiten.SetWindowFloating(floating)
  274. ebiten.SetScreenClearedEveryFrame(screenCleared)
  275. if maximize && ebiten.WindowResizingMode() == ebiten.WindowResizingModeEnabled {
  276. ebiten.MaximizeWindow()
  277. }
  278. if minimize {
  279. ebiten.MinimizeWindow()
  280. }
  281. if restore {
  282. ebiten.RestoreWindow()
  283. }
  284. ebiten.SetWindowResizingMode(resizingMode)
  285. if inpututil.IsKeyJustPressed(ebiten.KeyI) {
  286. ebiten.SetWindowIcon([]image.Image{createRandomIconImage()})
  287. }
  288. if inpututil.IsKeyJustPressed(ebiten.KeyJ) {
  289. ebiten.SetWindowIcon(nil)
  290. }
  291. ebiten.SetWindowMousePassthrough(mousePassthrough)
  292. g.count++
  293. return nil
  294. }
  295. func (g *game) Draw(screen *ebiten.Image) {
  296. w, h := gophersImage.Bounds().Dx(), gophersImage.Bounds().Dy()
  297. w2, h2 := screen.Bounds().Dx(), screen.Bounds().Dy()
  298. op := &ebiten.DrawImageOptions{}
  299. op.GeoM.Translate(float64(-w+w2)/2, float64(-h+h2)/2)
  300. dx := math.Cos(2*math.Pi*float64(g.count)/360) * 20
  301. dy := math.Sin(2*math.Pi*float64(g.count)/360) * 20
  302. op.GeoM.Translate(dx, dy)
  303. screen.DrawImage(gophersImage, op)
  304. wx, wy := ebiten.WindowPosition()
  305. ww, wh := ebiten.WindowSize()
  306. minw, minh, maxw, maxh := ebiten.WindowSizeLimits()
  307. cx, cy := ebiten.CursorPosition()
  308. tpsStr := "Sync with FPS"
  309. if t := ebiten.TPS(); t != ebiten.SyncWithFPS {
  310. tpsStr = fmt.Sprintf("%d", t)
  311. }
  312. var lines []string
  313. if !ebiten.IsWindowMaximized() && ebiten.WindowResizingMode() == ebiten.WindowResizingModeEnabled {
  314. lines = append(lines, "[M] Maximize the window (only for desktops)")
  315. }
  316. if !ebiten.IsWindowMinimized() {
  317. lines = append(lines, "[N] Minimize the window (only for desktops)")
  318. }
  319. if ebiten.IsWindowMaximized() || ebiten.IsWindowMinimized() {
  320. lines = append(lines, "[E] Restore the window from maximized/minimized state (only for desktops)")
  321. }
  322. msgM := strings.Join(lines, "\n")
  323. msgR := "[R] Switch the window resizing mode (only for desktops)\n"
  324. fg := "Yes"
  325. if !ebiten.IsFocused() {
  326. fg = "No"
  327. }
  328. msg := fmt.Sprintf(`[Arrow keys] Move the window
  329. [Shift + Arrow keys] Change the window size
  330. %s
  331. [F] Switch the fullscreen state
  332. [U] Switch the runnable-on-unfocused state
  333. [C] Switch the cursor mode (visible, hidden, or captured)
  334. [I] Change the window icon (only for desktops)
  335. [J] Reset the window icon (only for desktops)
  336. [V] Switch the vsync
  337. [T] Switch TPS (ticks per second)
  338. [D] Switch the window decoration (only for desktops)
  339. [L] Switch the window floating state (only for desktops)
  340. [W] Switch whether to skip clearing the screen
  341. [P] Switch whether a mouse cursor passthroughs the window (only for desktops)
  342. %s
  343. IsFocused?: %s
  344. Window Position: (%d, %d)
  345. Window Size: (%d, %d)
  346. Window size limitation: (%d, %d) - (%d, %d)
  347. Cursor: (%d, %d)
  348. TPS: Current: %0.2f / Max: %s
  349. FPS: %0.2f
  350. Device Scale Factor: %0.2f`, msgM, msgR, fg, wx, wy, ww, wh, minw, minh, maxw, maxh, cx, cy, ebiten.ActualTPS(), tpsStr, ebiten.ActualFPS(), ebiten.Monitor().DeviceScaleFactor())
  351. ebitenutil.DebugPrint(screen, msg)
  352. }
  353. func parseWindowPosition() (int, int, bool) {
  354. if *flagWindowPosition == "" {
  355. return 0, 0, false
  356. }
  357. tokens := strings.Split(*flagWindowPosition, ",")
  358. if len(tokens) != 2 {
  359. return 0, 0, false
  360. }
  361. x, err := strconv.Atoi(tokens[0])
  362. if err != nil {
  363. return 0, 0, false
  364. }
  365. y, err := strconv.Atoi(tokens[1])
  366. if err != nil {
  367. return 0, 0, false
  368. }
  369. return x, y, true
  370. }
  371. func main() {
  372. fmt.Printf("Device scale factor: %0.2f\n", ebiten.Monitor().DeviceScaleFactor())
  373. w, h := ebiten.Monitor().Size()
  374. fmt.Printf("Screen size in fullscreen: %d, %d\n", w, h)
  375. // Decode an image from the image file's byte slice.
  376. img, _, err := image.Decode(bytes.NewReader(images.Gophers_jpg))
  377. if err != nil {
  378. log.Fatal(err)
  379. }
  380. gophersImage = ebiten.NewImageFromImage(img)
  381. ebiten.SetWindowIcon([]image.Image{createRandomIconImage()})
  382. if x, y, ok := parseWindowPosition(); ok {
  383. ebiten.SetWindowPosition(x, y)
  384. }
  385. g := &game{
  386. width: initScreenWidth,
  387. height: initScreenHeight,
  388. }
  389. if *flagFullscreen {
  390. ebiten.SetFullscreen(true)
  391. }
  392. if *flagResizable {
  393. ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
  394. }
  395. if *flagFloating {
  396. ebiten.SetWindowFloating(true)
  397. }
  398. if *flagMaximize {
  399. ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
  400. ebiten.MaximizeWindow()
  401. }
  402. if !*flagVsync {
  403. ebiten.SetVsyncEnabled(false)
  404. }
  405. if *flagAutoAdjusting {
  406. ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
  407. }
  408. if !*flagRunnableOnUnfocused {
  409. ebiten.SetRunnableOnUnfocused(false)
  410. }
  411. minw, minh, maxw, maxh := -1, -1, -1, -1
  412. reSize := regexp.MustCompile(`^(\d+)x(\d+)$`)
  413. if m := reSize.FindStringSubmatch(*flagMinWindowSize); m != nil {
  414. minw, _ = strconv.Atoi(m[1])
  415. minh, _ = strconv.Atoi(m[2])
  416. }
  417. if m := reSize.FindStringSubmatch(*flagMaxWindowSize); m != nil {
  418. maxw, _ = strconv.Atoi(m[1])
  419. maxh, _ = strconv.Atoi(m[2])
  420. }
  421. if minw >= 0 || minh >= 0 || maxw >= 0 || maxh >= 0 {
  422. ebiten.SetWindowSizeLimits(minw, minh, maxw, maxh)
  423. ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
  424. }
  425. op := &ebiten.RunGameOptions{}
  426. switch *flagGraphicsLibrary {
  427. case "":
  428. op.GraphicsLibrary = ebiten.GraphicsLibraryAuto
  429. case "opengl":
  430. op.GraphicsLibrary = ebiten.GraphicsLibraryOpenGL
  431. case "directx":
  432. op.GraphicsLibrary = ebiten.GraphicsLibraryDirectX
  433. case "metal":
  434. op.GraphicsLibrary = ebiten.GraphicsLibraryMetal
  435. default:
  436. log.Fatalf("unexpected graphics library: %s", *flagGraphicsLibrary)
  437. }
  438. switch *flagColorSpace {
  439. case "":
  440. op.ColorSpace = ebiten.ColorSpaceDefault
  441. case "srgb":
  442. op.ColorSpace = ebiten.ColorSpaceSRGB
  443. case "display-p3":
  444. op.ColorSpace = ebiten.ColorSpaceDisplayP3
  445. }
  446. op.InitUnfocused = !*flagInitFocused
  447. op.ScreenTransparent = *flagTransparent
  448. op.X11ClassName = "Window-Size"
  449. op.X11InstanceName = "window-size"
  450. const title = "Window Size (Ebitengine Demo)"
  451. ww := int(float64(g.width) * initScreenScale)
  452. wh := int(float64(g.height) * initScreenScale)
  453. ebiten.SetWindowSize(ww, wh)
  454. ebiten.SetWindowTitle(title)
  455. if err := ebiten.RunGameWithOptions(g, op); err != nil {
  456. log.Fatal(err)
  457. }
  458. }