| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 |
- // 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.
- //go:build darwin || linux
- // +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 acceleration
- 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
- }
- }
|