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>
This commit is contained in:
parent
44f634ba28
commit
746653dcb3
2
example/flappy/assets/README
Normal file
2
example/flappy/assets/README
Normal file
@ -0,0 +1,2 @@
|
||||
The sprites were created by Renee French and are distributed
|
||||
under the Creative Commons Attributions 3.0 license.
|
BIN
example/flappy/assets/sprite.png
Normal file
BIN
example/flappy/assets/sprite.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 75 KiB |
354
example/flappy/game.go
Normal file
354
example/flappy/game.go
Normal file
@ -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
|
||||
}
|
||||
}
|
98
example/flappy/main.go
Normal file
98
example/flappy/main.go
Normal file
@ -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)
|
||||
}
|
10
example/flappy/main_x.go
Normal file
10
example/flappy/main_x.go
Normal file
@ -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() {
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user