main.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. // Copyright 2018 The Ebiten Authors
  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. _ "embed"
  18. "flag"
  19. "fmt"
  20. "image"
  21. "image/color"
  22. _ "image/png"
  23. "log"
  24. "math"
  25. "math/rand/v2"
  26. "github.com/hajimehoshi/ebiten/v2"
  27. "github.com/hajimehoshi/ebiten/v2/audio"
  28. "github.com/hajimehoshi/ebiten/v2/audio/vorbis"
  29. "github.com/hajimehoshi/ebiten/v2/audio/wav"
  30. "github.com/hajimehoshi/ebiten/v2/ebitenutil"
  31. raudio "github.com/hajimehoshi/ebiten/v2/examples/resources/audio"
  32. "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
  33. resources "github.com/hajimehoshi/ebiten/v2/examples/resources/images/flappy"
  34. "github.com/hajimehoshi/ebiten/v2/inpututil"
  35. "github.com/hajimehoshi/ebiten/v2/text/v2"
  36. )
  37. var flagCRT = flag.Bool("crt", false, "enable the CRT effect")
  38. //go:embed crt.go
  39. var crtGo []byte
  40. func floorDiv(x, y int) int {
  41. d := x / y
  42. if d*y == x || x >= 0 {
  43. return d
  44. }
  45. return d - 1
  46. }
  47. func floorMod(x, y int) int {
  48. return x - floorDiv(x, y)*y
  49. }
  50. const (
  51. screenWidth = 640
  52. screenHeight = 480
  53. tileSize = 32
  54. titleFontSize = fontSize * 1.5
  55. fontSize = 24
  56. smallFontSize = fontSize / 2
  57. pipeWidth = tileSize * 2
  58. pipeStartOffsetX = 8
  59. pipeIntervalX = 8
  60. pipeGapY = 5
  61. )
  62. var (
  63. gopherImage *ebiten.Image
  64. tilesImage *ebiten.Image
  65. arcadeFaceSource *text.GoTextFaceSource
  66. )
  67. func init() {
  68. img, _, err := image.Decode(bytes.NewReader(resources.Gopher_png))
  69. if err != nil {
  70. log.Fatal(err)
  71. }
  72. gopherImage = ebiten.NewImageFromImage(img)
  73. img, _, err = image.Decode(bytes.NewReader(resources.Tiles_png))
  74. if err != nil {
  75. log.Fatal(err)
  76. }
  77. tilesImage = ebiten.NewImageFromImage(img)
  78. }
  79. func init() {
  80. s, err := text.NewGoTextFaceSource(bytes.NewReader(fonts.PressStart2P_ttf))
  81. if err != nil {
  82. log.Fatal(err)
  83. }
  84. arcadeFaceSource = s
  85. }
  86. type Mode int
  87. const (
  88. ModeTitle Mode = iota
  89. ModeGame
  90. ModeGameOver
  91. )
  92. type Game struct {
  93. mode Mode
  94. // The gopher's position
  95. x16 int
  96. y16 int
  97. vy16 int
  98. // Camera
  99. cameraX int
  100. cameraY int
  101. // Pipes
  102. pipeTileYs []int
  103. gameoverCount int
  104. touchIDs []ebiten.TouchID
  105. gamepadIDs []ebiten.GamepadID
  106. audioContext *audio.Context
  107. jumpPlayer *audio.Player
  108. hitPlayer *audio.Player
  109. }
  110. func NewGame(crt bool) ebiten.Game {
  111. g := &Game{}
  112. g.init()
  113. if crt {
  114. return &GameWithCRTEffect{Game: g}
  115. }
  116. return g
  117. }
  118. func (g *Game) init() {
  119. g.x16 = 0
  120. g.y16 = 100 * 16
  121. g.cameraX = -240
  122. g.cameraY = 0
  123. g.pipeTileYs = make([]int, 256)
  124. for i := range g.pipeTileYs {
  125. g.pipeTileYs[i] = rand.IntN(6) + 2
  126. }
  127. if g.audioContext == nil {
  128. g.audioContext = audio.NewContext(48000)
  129. }
  130. jumpD, err := vorbis.DecodeF32(bytes.NewReader(raudio.Jump_ogg))
  131. if err != nil {
  132. log.Fatal(err)
  133. }
  134. g.jumpPlayer, err = g.audioContext.NewPlayerF32(jumpD)
  135. if err != nil {
  136. log.Fatal(err)
  137. }
  138. jabD, err := wav.DecodeF32(bytes.NewReader(raudio.Jab_wav))
  139. if err != nil {
  140. log.Fatal(err)
  141. }
  142. g.hitPlayer, err = g.audioContext.NewPlayerF32(jabD)
  143. if err != nil {
  144. log.Fatal(err)
  145. }
  146. }
  147. func (g *Game) isKeyJustPressed() bool {
  148. if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
  149. return true
  150. }
  151. if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
  152. return true
  153. }
  154. g.touchIDs = inpututil.AppendJustPressedTouchIDs(g.touchIDs[:0])
  155. if len(g.touchIDs) > 0 {
  156. return true
  157. }
  158. g.gamepadIDs = ebiten.AppendGamepadIDs(g.gamepadIDs[:0])
  159. for _, g := range g.gamepadIDs {
  160. if ebiten.IsStandardGamepadLayoutAvailable(g) {
  161. if inpututil.IsStandardGamepadButtonJustPressed(g, ebiten.StandardGamepadButtonRightBottom) {
  162. return true
  163. }
  164. if inpututil.IsStandardGamepadButtonJustPressed(g, ebiten.StandardGamepadButtonRightRight) {
  165. return true
  166. }
  167. } else {
  168. // The button 0/1 might not be A/B buttons.
  169. if inpututil.IsGamepadButtonJustPressed(g, ebiten.GamepadButton0) {
  170. return true
  171. }
  172. if inpututil.IsGamepadButtonJustPressed(g, ebiten.GamepadButton1) {
  173. return true
  174. }
  175. }
  176. }
  177. return false
  178. }
  179. func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
  180. return screenWidth, screenHeight
  181. }
  182. func (g *Game) Update() error {
  183. switch g.mode {
  184. case ModeTitle:
  185. if g.isKeyJustPressed() {
  186. g.mode = ModeGame
  187. }
  188. case ModeGame:
  189. g.x16 += 32
  190. g.cameraX += 2
  191. if g.isKeyJustPressed() {
  192. g.vy16 = -96
  193. if err := g.jumpPlayer.Rewind(); err != nil {
  194. return err
  195. }
  196. g.jumpPlayer.Play()
  197. }
  198. g.y16 += g.vy16
  199. // Gravity
  200. g.vy16 += 4
  201. if g.vy16 > 96 {
  202. g.vy16 = 96
  203. }
  204. if g.hit() {
  205. if err := g.hitPlayer.Rewind(); err != nil {
  206. return err
  207. }
  208. g.hitPlayer.Play()
  209. g.mode = ModeGameOver
  210. g.gameoverCount = 30
  211. }
  212. case ModeGameOver:
  213. if g.gameoverCount > 0 {
  214. g.gameoverCount--
  215. }
  216. if g.gameoverCount == 0 && g.isKeyJustPressed() {
  217. g.init()
  218. g.mode = ModeTitle
  219. }
  220. }
  221. return nil
  222. }
  223. func (g *Game) Draw(screen *ebiten.Image) {
  224. screen.Fill(color.RGBA{0x80, 0xa0, 0xc0, 0xff})
  225. g.drawTiles(screen)
  226. if g.mode != ModeTitle {
  227. g.drawGopher(screen)
  228. }
  229. var titleTexts string
  230. var texts string
  231. switch g.mode {
  232. case ModeTitle:
  233. titleTexts = "FLAPPY GOPHER"
  234. texts = "\n\n\n\n\n\nPRESS SPACE KEY\n\nOR A/B BUTTON\n\nOR TOUCH SCREEN"
  235. case ModeGameOver:
  236. texts = "\nGAME OVER!"
  237. }
  238. op := &text.DrawOptions{}
  239. op.GeoM.Translate(screenWidth/2, 3*titleFontSize)
  240. op.ColorScale.ScaleWithColor(color.White)
  241. op.LineSpacing = titleFontSize
  242. op.PrimaryAlign = text.AlignCenter
  243. text.Draw(screen, titleTexts, &text.GoTextFace{
  244. Source: arcadeFaceSource,
  245. Size: titleFontSize,
  246. }, op)
  247. op = &text.DrawOptions{}
  248. op.GeoM.Translate(screenWidth/2, 3*titleFontSize)
  249. op.ColorScale.ScaleWithColor(color.White)
  250. op.LineSpacing = fontSize
  251. op.PrimaryAlign = text.AlignCenter
  252. text.Draw(screen, texts, &text.GoTextFace{
  253. Source: arcadeFaceSource,
  254. Size: fontSize,
  255. }, op)
  256. if g.mode == ModeTitle {
  257. const msg = "Go Gopher by Renee French is\nlicenced under CC BY 3.0."
  258. op := &text.DrawOptions{}
  259. op.GeoM.Translate(screenWidth/2, screenHeight-smallFontSize/2)
  260. op.ColorScale.ScaleWithColor(color.White)
  261. op.LineSpacing = smallFontSize
  262. op.PrimaryAlign = text.AlignCenter
  263. op.SecondaryAlign = text.AlignEnd
  264. text.Draw(screen, msg, &text.GoTextFace{
  265. Source: arcadeFaceSource,
  266. Size: smallFontSize,
  267. }, op)
  268. }
  269. op = &text.DrawOptions{}
  270. op.GeoM.Translate(screenWidth, 0)
  271. op.ColorScale.ScaleWithColor(color.White)
  272. op.LineSpacing = fontSize
  273. op.PrimaryAlign = text.AlignEnd
  274. text.Draw(screen, fmt.Sprintf("%04d", g.score()), &text.GoTextFace{
  275. Source: arcadeFaceSource,
  276. Size: fontSize,
  277. }, op)
  278. ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f", ebiten.ActualTPS()))
  279. }
  280. func (g *Game) pipeAt(tileX int) (tileY int, ok bool) {
  281. if (tileX - pipeStartOffsetX) <= 0 {
  282. return 0, false
  283. }
  284. if floorMod(tileX-pipeStartOffsetX, pipeIntervalX) != 0 {
  285. return 0, false
  286. }
  287. idx := floorDiv(tileX-pipeStartOffsetX, pipeIntervalX)
  288. return g.pipeTileYs[idx%len(g.pipeTileYs)], true
  289. }
  290. func (g *Game) score() int {
  291. x := floorDiv(g.x16, 16) / tileSize
  292. if (x - pipeStartOffsetX) <= 0 {
  293. return 0
  294. }
  295. return floorDiv(x-pipeStartOffsetX, pipeIntervalX)
  296. }
  297. func (g *Game) hit() bool {
  298. if g.mode != ModeGame {
  299. return false
  300. }
  301. const (
  302. gopherWidth = 30
  303. gopherHeight = 60
  304. )
  305. w, h := gopherImage.Bounds().Dx(), gopherImage.Bounds().Dy()
  306. x0 := floorDiv(g.x16, 16) + (w-gopherWidth)/2
  307. y0 := floorDiv(g.y16, 16) + (h-gopherHeight)/2
  308. x1 := x0 + gopherWidth
  309. y1 := y0 + gopherHeight
  310. if y0 < -tileSize*4 {
  311. return true
  312. }
  313. if y1 >= screenHeight-tileSize {
  314. return true
  315. }
  316. xMin := floorDiv(x0-pipeWidth, tileSize)
  317. xMax := floorDiv(x0+gopherWidth, tileSize)
  318. for x := xMin; x <= xMax; x++ {
  319. y, ok := g.pipeAt(x)
  320. if !ok {
  321. continue
  322. }
  323. if x0 >= x*tileSize+pipeWidth {
  324. continue
  325. }
  326. if x1 < x*tileSize {
  327. continue
  328. }
  329. if y0 < y*tileSize {
  330. return true
  331. }
  332. if y1 >= (y+pipeGapY)*tileSize {
  333. return true
  334. }
  335. }
  336. return false
  337. }
  338. func (g *Game) drawTiles(screen *ebiten.Image) {
  339. const (
  340. nx = screenWidth / tileSize
  341. ny = screenHeight / tileSize
  342. pipeTileSrcX = 128
  343. pipeTileSrcY = 192
  344. )
  345. op := &ebiten.DrawImageOptions{}
  346. for i := -2; i < nx+1; i++ {
  347. // ground
  348. op.GeoM.Reset()
  349. op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)),
  350. float64((ny-1)*tileSize-floorMod(g.cameraY, tileSize)))
  351. screen.DrawImage(tilesImage.SubImage(image.Rect(0, 0, tileSize, tileSize)).(*ebiten.Image), op)
  352. // pipe
  353. if tileY, ok := g.pipeAt(floorDiv(g.cameraX, tileSize) + i); ok {
  354. for j := 0; j < tileY; j++ {
  355. op.GeoM.Reset()
  356. op.GeoM.Scale(1, -1)
  357. op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)),
  358. float64(j*tileSize-floorMod(g.cameraY, tileSize)))
  359. op.GeoM.Translate(0, tileSize)
  360. var r image.Rectangle
  361. if j == tileY-1 {
  362. r = image.Rect(pipeTileSrcX, pipeTileSrcY, pipeTileSrcX+tileSize*2, pipeTileSrcY+tileSize)
  363. } else {
  364. r = image.Rect(pipeTileSrcX, pipeTileSrcY+tileSize, pipeTileSrcX+tileSize*2, pipeTileSrcY+tileSize*2)
  365. }
  366. screen.DrawImage(tilesImage.SubImage(r).(*ebiten.Image), op)
  367. }
  368. for j := tileY + pipeGapY; j < screenHeight/tileSize-1; j++ {
  369. op.GeoM.Reset()
  370. op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)),
  371. float64(j*tileSize-floorMod(g.cameraY, tileSize)))
  372. var r image.Rectangle
  373. if j == tileY+pipeGapY {
  374. r = image.Rect(pipeTileSrcX, pipeTileSrcY, pipeTileSrcX+pipeWidth, pipeTileSrcY+tileSize)
  375. } else {
  376. r = image.Rect(pipeTileSrcX, pipeTileSrcY+tileSize, pipeTileSrcX+pipeWidth, pipeTileSrcY+tileSize+tileSize)
  377. }
  378. screen.DrawImage(tilesImage.SubImage(r).(*ebiten.Image), op)
  379. }
  380. }
  381. }
  382. }
  383. func (g *Game) drawGopher(screen *ebiten.Image) {
  384. op := &ebiten.DrawImageOptions{}
  385. w, h := gopherImage.Bounds().Dx(), gopherImage.Bounds().Dy()
  386. op.GeoM.Translate(-float64(w)/2.0, -float64(h)/2.0)
  387. op.GeoM.Rotate(float64(g.vy16) / 96.0 * math.Pi / 6)
  388. op.GeoM.Translate(float64(w)/2.0, float64(h)/2.0)
  389. op.GeoM.Translate(float64(g.x16/16.0)-float64(g.cameraX), float64(g.y16/16.0)-float64(g.cameraY))
  390. op.Filter = ebiten.FilterLinear
  391. screen.DrawImage(gopherImage, op)
  392. }
  393. type GameWithCRTEffect struct {
  394. ebiten.Game
  395. crtShader *ebiten.Shader
  396. }
  397. func (g *GameWithCRTEffect) DrawFinalScreen(screen ebiten.FinalScreen, offscreen *ebiten.Image, geoM ebiten.GeoM) {
  398. if g.crtShader == nil {
  399. s, err := ebiten.NewShader(crtGo)
  400. if err != nil {
  401. panic(fmt.Sprintf("flappy: failed to compiled the CRT shader: %v", err))
  402. }
  403. g.crtShader = s
  404. }
  405. os := offscreen.Bounds().Size()
  406. op := &ebiten.DrawRectShaderOptions{}
  407. op.Images[0] = offscreen
  408. op.GeoM = geoM
  409. screen.DrawRectShader(os.X, os.Y, g.crtShader, op)
  410. }
  411. func main() {
  412. flag.Parse()
  413. ebiten.SetWindowSize(screenWidth, screenHeight)
  414. ebiten.SetWindowTitle("Flappy Gopher (Ebitengine Demo)")
  415. if err := ebiten.RunGame(NewGame(*flagCRT)); err != nil {
  416. panic(err)
  417. }
  418. }