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