// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build darwin linux package main import ( "image" "log" "math" "math/rand" _ "image/png" "golang.org/x/mobile/asset" "golang.org/x/mobile/exp/f32" "golang.org/x/mobile/exp/sprite" "golang.org/x/mobile/exp/sprite/clock" ) const ( tileWidth, tileHeight = 16, 16 // width and height of each tile tilesX, tilesY = 16, 16 // number of horizontal tiles gopherTile = 1 // which tile the gopher is standing on (0-indexed) initScrollV = 1 // initial scroll velocity scrollA = 0.001 // scroll accelleration gravity = 0.1 // gravity jumpV = -5 // jump velocity flapV = -1.5 // flap velocity deadScrollA = -0.01 // scroll deceleration after the gopher dies deadTimeBeforeReset = 240 // how long to wait before restarting the game groundChangeProb = 5 // 1/probability of ground height change groundWobbleProb = 3 // 1/probability of minor ground height change groundMin = tileHeight * (tilesY - 2*tilesY/5) groundMax = tileHeight * tilesY initGroundY = tileHeight * (tilesY - 1) climbGrace = tileHeight / 3 // gopher won't die if it hits a cliff this high ) type Game struct { gopher struct { y float32 // y-offset v float32 // velocity atRest bool // is the gopher on the ground? flapped bool // has the gopher flapped since it became airborne? dead bool // is the gopher dead? deadTime clock.Time // when the gopher died } scroll struct { x float32 // x-offset v float32 // velocity } groundY [tilesX + 3]float32 // ground y-offsets groundTex [tilesX + 3]int // ground texture lastCalc clock.Time // when we last calculated a frame } func NewGame() *Game { var g Game g.reset() return &g } func (g *Game) reset() { g.gopher.y = 0 g.gopher.v = 0 g.scroll.x = 0 g.scroll.v = initScrollV for i := range g.groundY { g.groundY[i] = initGroundY g.groundTex[i] = randomGroundTexture() } g.gopher.atRest = false g.gopher.flapped = false g.gopher.dead = false g.gopher.deadTime = 0 } func (g *Game) Scene(eng sprite.Engine) *sprite.Node { texs := loadTextures(eng) scene := &sprite.Node{} eng.Register(scene) eng.SetTransform(scene, f32.Affine{ {1, 0, 0}, {0, 1, 0}, }) newNode := func(fn arrangerFunc) { n := &sprite.Node{Arranger: arrangerFunc(fn)} eng.Register(n) scene.AppendChild(n) } // The ground. for i := range g.groundY { i := i // The top of the ground. newNode(func(eng sprite.Engine, n *sprite.Node, t clock.Time) { eng.SetSubTex(n, texs[g.groundTex[i]]) eng.SetTransform(n, f32.Affine{ {tileWidth, 0, float32(i)*tileWidth - g.scroll.x}, {0, tileHeight, g.groundY[i]}, }) }) // The earth beneath. newNode(func(eng sprite.Engine, n *sprite.Node, t clock.Time) { eng.SetSubTex(n, texs[texEarth]) eng.SetTransform(n, f32.Affine{ {tileWidth, 0, float32(i)*tileWidth - g.scroll.x}, {0, tileHeight * tilesY, g.groundY[i] + tileHeight}, }) }) } // The gopher. newNode(func(eng sprite.Engine, n *sprite.Node, t clock.Time) { a := f32.Affine{ {tileWidth * 2, 0, tileWidth*(gopherTile-1) + tileWidth/8}, {0, tileHeight * 2, g.gopher.y - tileHeight + tileHeight/4}, } var x int switch { case g.gopher.dead: x = frame(t, 16, texGopherDead1, texGopherDead2) animateDeadGopher(&a, t-g.gopher.deadTime) case g.gopher.v < 0: x = frame(t, 4, texGopherFlap1, texGopherFlap2) case g.gopher.atRest: x = frame(t, 4, texGopherRun1, texGopherRun2) default: x = frame(t, 8, texGopherRun1, texGopherRun2) } eng.SetSubTex(n, texs[x]) eng.SetTransform(n, a) }) return scene } // frame returns the frame for the given time t // when each frame is displayed for duration d. func frame(t, d clock.Time, frames ...int) int { total := int(d) * len(frames) return frames[(int(t)%total)/int(d)] } func animateDeadGopher(a *f32.Affine, t clock.Time) { dt := float32(t) a.Scale(a, 1+dt/20, 1+dt/20) a.Translate(a, 0.5, 0.5) a.Rotate(a, dt/math.Pi/-8) a.Translate(a, -0.5, -0.5) } type arrangerFunc func(e sprite.Engine, n *sprite.Node, t clock.Time) func (a arrangerFunc) Arrange(e sprite.Engine, n *sprite.Node, t clock.Time) { a(e, n, t) } const ( texGopherRun1 = iota texGopherRun2 texGopherFlap1 texGopherFlap2 texGopherDead1 texGopherDead2 texGround1 texGround2 texGround3 texGround4 texEarth ) func randomGroundTexture() int { return texGround1 + rand.Intn(4) } func loadTextures(eng sprite.Engine) []sprite.SubTex { a, err := asset.Open("sprite.png") if err != nil { log.Fatal(err) } defer a.Close() m, _, err := image.Decode(a) if err != nil { log.Fatal(err) } t, err := eng.LoadTexture(m) if err != nil { log.Fatal(err) } const n = 128 // The +1's and -1's in the rectangles below are to prevent colors from // adjacent textures leaking into a given texture. // See: http://stackoverflow.com/questions/19611745/opengl-black-lines-in-between-tiles return []sprite.SubTex{ texGopherRun1: sprite.SubTex{t, image.Rect(n*0+1, 0, n*1-1, n)}, texGopherRun2: sprite.SubTex{t, image.Rect(n*1+1, 0, n*2-1, n)}, texGopherFlap1: sprite.SubTex{t, image.Rect(n*2+1, 0, n*3-1, n)}, texGopherFlap2: sprite.SubTex{t, image.Rect(n*3+1, 0, n*4-1, n)}, texGopherDead1: sprite.SubTex{t, image.Rect(n*4+1, 0, n*5-1, n)}, texGopherDead2: sprite.SubTex{t, image.Rect(n*5+1, 0, n*6-1, n)}, texGround1: sprite.SubTex{t, image.Rect(n*6+1, 0, n*7-1, n)}, texGround2: sprite.SubTex{t, image.Rect(n*7+1, 0, n*8-1, n)}, texGround3: sprite.SubTex{t, image.Rect(n*8+1, 0, n*9-1, n)}, texGround4: sprite.SubTex{t, image.Rect(n*9+1, 0, n*10-1, n)}, texEarth: sprite.SubTex{t, image.Rect(n*10+1, 0, n*11-1, n)}, } } func (g *Game) Press(down bool) { if g.gopher.dead { // Player can't control a dead gopher. return } if down { switch { case g.gopher.atRest: // Gopher may jump from the ground. g.gopher.v = jumpV case !g.gopher.flapped: // Gopher may flap once in mid-air. g.gopher.flapped = true g.gopher.v = flapV } } else { // Stop gopher rising on button release. if g.gopher.v < 0 { g.gopher.v = 0 } } } func (g *Game) Update(now clock.Time) { if g.gopher.dead && now-g.gopher.deadTime > deadTimeBeforeReset { // Restart if the gopher has been dead for a while. g.reset() } // Compute game states up to now. for ; g.lastCalc < now; g.lastCalc++ { g.calcFrame() } } func (g *Game) calcFrame() { g.calcScroll() g.calcGopher() } func (g *Game) calcScroll() { // Compute velocity. if g.gopher.dead { // Decrease scroll speed when the gopher dies. g.scroll.v += deadScrollA if g.scroll.v < 0 { g.scroll.v = 0 } } else { // Increase scroll speed. g.scroll.v += scrollA } // Compute offset. g.scroll.x += g.scroll.v // Create new ground tiles if we need to. for g.scroll.x > tileWidth { g.newGroundTile() // Check whether the gopher has crashed. // Do this for each new ground tile so that when the scroll // velocity is >tileWidth/frame it can't pass through the ground. if !g.gopher.dead && g.gopherCrashed() { g.killGopher() } } } func (g *Game) calcGopher() { // Compute velocity. g.gopher.v += gravity // Compute offset. g.gopher.y += g.gopher.v g.clampToGround() } func (g *Game) newGroundTile() { // Compute next ground y-offset. next := g.nextGroundY() nextTex := randomGroundTexture() // Shift ground tiles to the left. g.scroll.x -= tileWidth copy(g.groundY[:], g.groundY[1:]) copy(g.groundTex[:], g.groundTex[1:]) last := len(g.groundY) - 1 g.groundY[last] = next g.groundTex[last] = nextTex } func (g *Game) nextGroundY() float32 { prev := g.groundY[len(g.groundY)-1] if change := rand.Intn(groundChangeProb) == 0; change { return (groundMax-groundMin)*rand.Float32() + groundMin } if wobble := rand.Intn(groundWobbleProb) == 0; wobble { return prev + (rand.Float32()-0.5)*climbGrace } return prev } func (g *Game) gopherCrashed() bool { return g.gopher.y+tileHeight-climbGrace > g.groundY[gopherTile+1] } func (g *Game) killGopher() { g.gopher.dead = true g.gopher.deadTime = g.lastCalc g.gopher.v = jumpV * 1.5 // Bounce off screen. } func (g *Game) clampToGround() { if g.gopher.dead { // Allow the gopher to fall through ground when dead. return } // Compute the minimum offset of the ground beneath the gopher. minY := g.groundY[gopherTile] if y := g.groundY[gopherTile+1]; y < minY { minY = y } // Prevent the gopher from falling through the ground. maxGopherY := minY - tileHeight g.gopher.atRest = false if g.gopher.y >= maxGopherY { g.gopher.v = 0 g.gopher.y = maxGopherY g.gopher.atRest = true g.gopher.flapped = false } }