example/flappy: add simple example game
This game was developed for and presented at GoCon Winter 2015 in Tokyo.
Change-Id: I08148e16a54355b79f634dce867b3c3c0a0153cb
Reviewed-on: https://go-review.googlesource.com/18245
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/example/flappy/assets/README b/example/flappy/assets/README
new file mode 100644
index 0000000..cd66fc6
--- /dev/null
+++ b/example/flappy/assets/README
@@ -0,0 +1,2 @@
+The sprites were created by Renee French and are distributed
+under the Creative Commons Attributions 3.0 license.
diff --git a/example/flappy/assets/sprite.png b/example/flappy/assets/sprite.png
new file mode 100644
index 0000000..197e590
--- /dev/null
+++ b/example/flappy/assets/sprite.png
Binary files differ
diff --git a/example/flappy/game.go b/example/flappy/game.go
new file mode 100644
index 0000000..7edf5b1
--- /dev/null
+++ b/example/flappy/game.go
@@ -0,0 +1,354 @@
+// 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
+ // TODO(adg,nigeltao): remove +1's and -1's below once texture bleed issue is fixed
+ return []sprite.SubTex{
+ texGopherRun1: sprite.SubTex{t, image.Rect(n*0, 0, n*1, n)},
+ texGopherRun2: sprite.SubTex{t, image.Rect(n*1, 0, n*2, n)},
+ texGopherFlap1: sprite.SubTex{t, image.Rect(n*2, 0, n*3, n)},
+ texGopherFlap2: sprite.SubTex{t, image.Rect(n*3, 0, n*4, n)},
+ texGopherDead1: sprite.SubTex{t, image.Rect(n*4, 0, n*5, n)},
+ texGopherDead2: sprite.SubTex{t, image.Rect(n*5, 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
+ }
+}
diff --git a/example/flappy/main.go b/example/flappy/main.go
new file mode 100644
index 0000000..2274993
--- /dev/null
+++ b/example/flappy/main.go
@@ -0,0 +1,98 @@
+// 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
+
+// Flappy Gopher is a simple one-button game that uses the
+// mobile framework and the experimental sprite engine.
+package main
+
+import (
+ "math/rand"
+ "time"
+
+ "golang.org/x/mobile/app"
+ "golang.org/x/mobile/event/key"
+ "golang.org/x/mobile/event/lifecycle"
+ "golang.org/x/mobile/event/paint"
+ "golang.org/x/mobile/event/size"
+ "golang.org/x/mobile/event/touch"
+ "golang.org/x/mobile/exp/gl/glutil"
+ "golang.org/x/mobile/exp/sprite"
+ "golang.org/x/mobile/exp/sprite/clock"
+ "golang.org/x/mobile/exp/sprite/glsprite"
+ "golang.org/x/mobile/gl"
+)
+
+func main() {
+ rand.Seed(time.Now().UnixNano())
+
+ app.Main(func(a app.App) {
+ var glctx gl.Context
+ var sz size.Event
+ for e := range a.Events() {
+ switch e := a.Filter(e).(type) {
+ case lifecycle.Event:
+ switch e.Crosses(lifecycle.StageVisible) {
+ case lifecycle.CrossOn:
+ glctx, _ = e.DrawContext.(gl.Context)
+ onStart(glctx)
+ a.Send(paint.Event{})
+ case lifecycle.CrossOff:
+ onStop()
+ glctx = nil
+ }
+ case size.Event:
+ sz = e
+ case paint.Event:
+ if glctx == nil || e.External {
+ continue
+ }
+ onPaint(glctx, sz)
+ a.Publish()
+ a.Send(paint.Event{}) // keep animating
+ case touch.Event:
+ if down := e.Type == touch.TypeBegin; down || e.Type == touch.TypeEnd {
+ game.Press(down)
+ }
+ case key.Event:
+ if e.Code != key.CodeSpacebar {
+ break
+ }
+ if down := e.Direction == key.DirPress; down || e.Direction == key.DirRelease {
+ game.Press(down)
+ }
+ }
+ }
+ })
+}
+
+var (
+ startTime = time.Now()
+ images *glutil.Images
+ eng sprite.Engine
+ scene *sprite.Node
+ game *Game
+)
+
+func onStart(glctx gl.Context) {
+ images = glutil.NewImages(glctx)
+ eng = glsprite.Engine(images)
+ game = NewGame()
+ scene = game.Scene(eng)
+}
+
+func onStop() {
+ eng.Release()
+ images.Release()
+ game = nil
+}
+
+func onPaint(glctx gl.Context, sz size.Event) {
+ glctx.ClearColor(1, 1, 1, 1)
+ glctx.Clear(gl.COLOR_BUFFER_BIT)
+ now := clock.Time(time.Since(startTime) * 60 / time.Second)
+ game.Update(now)
+ eng.Render(scene, now, sz)
+}
diff --git a/example/flappy/main_x.go b/example/flappy/main_x.go
new file mode 100644
index 0000000..3d440e7
--- /dev/null
+++ b/example/flappy/main_x.go
@@ -0,0 +1,10 @@
+// Copyright 2014 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
+
+func main() {
+}