| // 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 |
| } |
| } |