game.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. // Copyright 2015 The Go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. //go:build darwin || linux
  5. package main
  6. import (
  7. "image"
  8. "log"
  9. "math"
  10. "math/rand"
  11. _ "image/png"
  12. "golang.org/x/mobile/asset"
  13. "golang.org/x/mobile/exp/f32"
  14. "golang.org/x/mobile/exp/sprite"
  15. "golang.org/x/mobile/exp/sprite/clock"
  16. )
  17. const (
  18. tileWidth, tileHeight = 16, 16 // width and height of each tile
  19. tilesX, tilesY = 16, 16 // number of horizontal tiles
  20. gopherTile = 1 // which tile the gopher is standing on (0-indexed)
  21. initScrollV = 1 // initial scroll velocity
  22. scrollA = 0.001 // scroll acceleration
  23. gravity = 0.1 // gravity
  24. jumpV = -5 // jump velocity
  25. flapV = -1.5 // flap velocity
  26. deadScrollA = -0.01 // scroll deceleration after the gopher dies
  27. deadTimeBeforeReset = 240 // how long to wait before restarting the game
  28. groundChangeProb = 5 // 1/probability of ground height change
  29. groundWobbleProb = 3 // 1/probability of minor ground height change
  30. groundMin = tileHeight * (tilesY - 2*tilesY/5)
  31. groundMax = tileHeight * tilesY
  32. initGroundY = tileHeight * (tilesY - 1)
  33. climbGrace = tileHeight / 3 // gopher won't die if it hits a cliff this high
  34. )
  35. type Game struct {
  36. gopher struct {
  37. y float32 // y-offset
  38. v float32 // velocity
  39. atRest bool // is the gopher on the ground?
  40. flapped bool // has the gopher flapped since it became airborne?
  41. dead bool // is the gopher dead?
  42. deadTime clock.Time // when the gopher died
  43. }
  44. scroll struct {
  45. x float32 // x-offset
  46. v float32 // velocity
  47. }
  48. groundY [tilesX + 3]float32 // ground y-offsets
  49. groundTex [tilesX + 3]int // ground texture
  50. lastCalc clock.Time // when we last calculated a frame
  51. }
  52. func NewGame() *Game {
  53. var g Game
  54. g.reset()
  55. return &g
  56. }
  57. func (g *Game) reset() {
  58. g.gopher.y = 0
  59. g.gopher.v = 0
  60. g.scroll.x = 0
  61. g.scroll.v = initScrollV
  62. for i := range g.groundY {
  63. g.groundY[i] = initGroundY
  64. g.groundTex[i] = randomGroundTexture()
  65. }
  66. g.gopher.atRest = false
  67. g.gopher.flapped = false
  68. g.gopher.dead = false
  69. g.gopher.deadTime = 0
  70. }
  71. func (g *Game) Scene(eng sprite.Engine) *sprite.Node {
  72. texs := loadTextures(eng)
  73. scene := &sprite.Node{}
  74. eng.Register(scene)
  75. eng.SetTransform(scene, f32.Affine{
  76. {1, 0, 0},
  77. {0, 1, 0},
  78. })
  79. newNode := func(fn arrangerFunc) {
  80. n := &sprite.Node{Arranger: arrangerFunc(fn)}
  81. eng.Register(n)
  82. scene.AppendChild(n)
  83. }
  84. // The ground.
  85. for i := range g.groundY {
  86. i := i
  87. // The top of the ground.
  88. newNode(func(eng sprite.Engine, n *sprite.Node, t clock.Time) {
  89. eng.SetSubTex(n, texs[g.groundTex[i]])
  90. eng.SetTransform(n, f32.Affine{
  91. {tileWidth, 0, float32(i)*tileWidth - g.scroll.x},
  92. {0, tileHeight, g.groundY[i]},
  93. })
  94. })
  95. // The earth beneath.
  96. newNode(func(eng sprite.Engine, n *sprite.Node, t clock.Time) {
  97. eng.SetSubTex(n, texs[texEarth])
  98. eng.SetTransform(n, f32.Affine{
  99. {tileWidth, 0, float32(i)*tileWidth - g.scroll.x},
  100. {0, tileHeight * tilesY, g.groundY[i] + tileHeight},
  101. })
  102. })
  103. }
  104. // The gopher.
  105. newNode(func(eng sprite.Engine, n *sprite.Node, t clock.Time) {
  106. a := f32.Affine{
  107. {tileWidth * 2, 0, tileWidth*(gopherTile-1) + tileWidth/8},
  108. {0, tileHeight * 2, g.gopher.y - tileHeight + tileHeight/4},
  109. }
  110. var x int
  111. switch {
  112. case g.gopher.dead:
  113. x = frame(t, 16, texGopherDead1, texGopherDead2)
  114. animateDeadGopher(&a, t-g.gopher.deadTime)
  115. case g.gopher.v < 0:
  116. x = frame(t, 4, texGopherFlap1, texGopherFlap2)
  117. case g.gopher.atRest:
  118. x = frame(t, 4, texGopherRun1, texGopherRun2)
  119. default:
  120. x = frame(t, 8, texGopherRun1, texGopherRun2)
  121. }
  122. eng.SetSubTex(n, texs[x])
  123. eng.SetTransform(n, a)
  124. })
  125. return scene
  126. }
  127. // frame returns the frame for the given time t
  128. // when each frame is displayed for duration d.
  129. func frame(t, d clock.Time, frames ...int) int {
  130. total := int(d) * len(frames)
  131. return frames[(int(t)%total)/int(d)]
  132. }
  133. func animateDeadGopher(a *f32.Affine, t clock.Time) {
  134. dt := float32(t)
  135. a.Scale(a, 1+dt/20, 1+dt/20)
  136. a.Translate(a, 0.5, 0.5)
  137. a.Rotate(a, dt/math.Pi/-8)
  138. a.Translate(a, -0.5, -0.5)
  139. }
  140. type arrangerFunc func(e sprite.Engine, n *sprite.Node, t clock.Time)
  141. func (a arrangerFunc) Arrange(e sprite.Engine, n *sprite.Node, t clock.Time) { a(e, n, t) }
  142. const (
  143. texGopherRun1 = iota
  144. texGopherRun2
  145. texGopherFlap1
  146. texGopherFlap2
  147. texGopherDead1
  148. texGopherDead2
  149. texGround1
  150. texGround2
  151. texGround3
  152. texGround4
  153. texEarth
  154. )
  155. func randomGroundTexture() int {
  156. return texGround1 + rand.Intn(4)
  157. }
  158. func loadTextures(eng sprite.Engine) []sprite.SubTex {
  159. a, err := asset.Open("sprite.png")
  160. if err != nil {
  161. log.Fatal(err)
  162. }
  163. defer a.Close()
  164. m, _, err := image.Decode(a)
  165. if err != nil {
  166. log.Fatal(err)
  167. }
  168. t, err := eng.LoadTexture(m)
  169. if err != nil {
  170. log.Fatal(err)
  171. }
  172. const n = 128
  173. // The +1's and -1's in the rectangles below are to prevent colors from
  174. // adjacent textures leaking into a given texture.
  175. // See: http://stackoverflow.com/questions/19611745/opengl-black-lines-in-between-tiles
  176. return []sprite.SubTex{
  177. texGopherRun1: sprite.SubTex{t, image.Rect(n*0+1, 0, n*1-1, n)},
  178. texGopherRun2: sprite.SubTex{t, image.Rect(n*1+1, 0, n*2-1, n)},
  179. texGopherFlap1: sprite.SubTex{t, image.Rect(n*2+1, 0, n*3-1, n)},
  180. texGopherFlap2: sprite.SubTex{t, image.Rect(n*3+1, 0, n*4-1, n)},
  181. texGopherDead1: sprite.SubTex{t, image.Rect(n*4+1, 0, n*5-1, n)},
  182. texGopherDead2: sprite.SubTex{t, image.Rect(n*5+1, 0, n*6-1, n)},
  183. texGround1: sprite.SubTex{t, image.Rect(n*6+1, 0, n*7-1, n)},
  184. texGround2: sprite.SubTex{t, image.Rect(n*7+1, 0, n*8-1, n)},
  185. texGround3: sprite.SubTex{t, image.Rect(n*8+1, 0, n*9-1, n)},
  186. texGround4: sprite.SubTex{t, image.Rect(n*9+1, 0, n*10-1, n)},
  187. texEarth: sprite.SubTex{t, image.Rect(n*10+1, 0, n*11-1, n)},
  188. }
  189. }
  190. func (g *Game) Press(down bool) {
  191. if g.gopher.dead {
  192. // Player can't control a dead gopher.
  193. return
  194. }
  195. if down {
  196. switch {
  197. case g.gopher.atRest:
  198. // Gopher may jump from the ground.
  199. g.gopher.v = jumpV
  200. case !g.gopher.flapped:
  201. // Gopher may flap once in mid-air.
  202. g.gopher.flapped = true
  203. g.gopher.v = flapV
  204. }
  205. } else {
  206. // Stop gopher rising on button release.
  207. if g.gopher.v < 0 {
  208. g.gopher.v = 0
  209. }
  210. }
  211. }
  212. func (g *Game) Update(now clock.Time) {
  213. if g.gopher.dead && now-g.gopher.deadTime > deadTimeBeforeReset {
  214. // Restart if the gopher has been dead for a while.
  215. g.reset()
  216. }
  217. // Compute game states up to now.
  218. for ; g.lastCalc < now; g.lastCalc++ {
  219. g.calcFrame()
  220. }
  221. }
  222. func (g *Game) calcFrame() {
  223. g.calcScroll()
  224. g.calcGopher()
  225. }
  226. func (g *Game) calcScroll() {
  227. // Compute velocity.
  228. if g.gopher.dead {
  229. // Decrease scroll speed when the gopher dies.
  230. g.scroll.v += deadScrollA
  231. if g.scroll.v < 0 {
  232. g.scroll.v = 0
  233. }
  234. } else {
  235. // Increase scroll speed.
  236. g.scroll.v += scrollA
  237. }
  238. // Compute offset.
  239. g.scroll.x += g.scroll.v
  240. // Create new ground tiles if we need to.
  241. for g.scroll.x > tileWidth {
  242. g.newGroundTile()
  243. // Check whether the gopher has crashed.
  244. // Do this for each new ground tile so that when the scroll
  245. // velocity is >tileWidth/frame it can't pass through the ground.
  246. if !g.gopher.dead && g.gopherCrashed() {
  247. g.killGopher()
  248. }
  249. }
  250. }
  251. func (g *Game) calcGopher() {
  252. // Compute velocity.
  253. g.gopher.v += gravity
  254. // Compute offset.
  255. g.gopher.y += g.gopher.v
  256. g.clampToGround()
  257. }
  258. func (g *Game) newGroundTile() {
  259. // Compute next ground y-offset.
  260. next := g.nextGroundY()
  261. nextTex := randomGroundTexture()
  262. // Shift ground tiles to the left.
  263. g.scroll.x -= tileWidth
  264. copy(g.groundY[:], g.groundY[1:])
  265. copy(g.groundTex[:], g.groundTex[1:])
  266. last := len(g.groundY) - 1
  267. g.groundY[last] = next
  268. g.groundTex[last] = nextTex
  269. }
  270. func (g *Game) nextGroundY() float32 {
  271. prev := g.groundY[len(g.groundY)-1]
  272. if change := rand.Intn(groundChangeProb) == 0; change {
  273. return (groundMax-groundMin)*rand.Float32() + groundMin
  274. }
  275. if wobble := rand.Intn(groundWobbleProb) == 0; wobble {
  276. return prev + (rand.Float32()-0.5)*climbGrace
  277. }
  278. return prev
  279. }
  280. func (g *Game) gopherCrashed() bool {
  281. return g.gopher.y+tileHeight-climbGrace > g.groundY[gopherTile+1]
  282. }
  283. func (g *Game) killGopher() {
  284. g.gopher.dead = true
  285. g.gopher.deadTime = g.lastCalc
  286. g.gopher.v = jumpV * 1.5 // Bounce off screen.
  287. }
  288. func (g *Game) clampToGround() {
  289. if g.gopher.dead {
  290. // Allow the gopher to fall through ground when dead.
  291. return
  292. }
  293. // Compute the minimum offset of the ground beneath the gopher.
  294. minY := g.groundY[gopherTile]
  295. if y := g.groundY[gopherTile+1]; y < minY {
  296. minY = y
  297. }
  298. // Prevent the gopher from falling through the ground.
  299. maxGopherY := minY - tileHeight
  300. g.gopher.atRest = false
  301. if g.gopher.y >= maxGopherY {
  302. g.gopher.v = 0
  303. g.gopher.y = maxGopherY
  304. g.gopher.atRest = true
  305. g.gopher.flapped = false
  306. }
  307. }