game.go 8.7 KB

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