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() {
+}