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