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 Binary files /dev/null and b/example/flappy/assets/sprite.png 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() { +}