github.com/SkycoinProject/gomobile@v0.0.0-20190312151609-d3739f865fa6/example/flappy/game.go (about)

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // +build darwin linux
     6  
     7  package main
     8  
     9  import (
    10  	"image"
    11  	"log"
    12  	"math"
    13  	"math/rand"
    14  
    15  	_ "image/png"
    16  
    17  	"golang.org/x/mobile/asset"
    18  	"golang.org/x/mobile/exp/f32"
    19  	"golang.org/x/mobile/exp/sprite"
    20  	"golang.org/x/mobile/exp/sprite/clock"
    21  )
    22  
    23  const (
    24  	tileWidth, tileHeight = 16, 16 // width and height of each tile
    25  	tilesX, tilesY        = 16, 16 // number of horizontal tiles
    26  
    27  	gopherTile = 1 // which tile the gopher is standing on (0-indexed)
    28  
    29  	initScrollV = 1     // initial scroll velocity
    30  	scrollA     = 0.001 // scroll accelleration
    31  	gravity     = 0.1   // gravity
    32  	jumpV       = -5    // jump velocity
    33  	flapV       = -1.5  // flap velocity
    34  
    35  	deadScrollA         = -0.01 // scroll deceleration after the gopher dies
    36  	deadTimeBeforeReset = 240   // how long to wait before restarting the game
    37  
    38  	groundChangeProb = 5 // 1/probability of ground height change
    39  	groundWobbleProb = 3 // 1/probability of minor ground height change
    40  	groundMin        = tileHeight * (tilesY - 2*tilesY/5)
    41  	groundMax        = tileHeight * tilesY
    42  	initGroundY      = tileHeight * (tilesY - 1)
    43  
    44  	climbGrace = tileHeight / 3 // gopher won't die if it hits a cliff this high
    45  )
    46  
    47  type Game struct {
    48  	gopher struct {
    49  		y        float32    // y-offset
    50  		v        float32    // velocity
    51  		atRest   bool       // is the gopher on the ground?
    52  		flapped  bool       // has the gopher flapped since it became airborne?
    53  		dead     bool       // is the gopher dead?
    54  		deadTime clock.Time // when the gopher died
    55  	}
    56  	scroll struct {
    57  		x float32 // x-offset
    58  		v float32 // velocity
    59  	}
    60  	groundY   [tilesX + 3]float32 // ground y-offsets
    61  	groundTex [tilesX + 3]int     // ground texture
    62  	lastCalc  clock.Time          // when we last calculated a frame
    63  }
    64  
    65  func NewGame() *Game {
    66  	var g Game
    67  	g.reset()
    68  	return &g
    69  }
    70  
    71  func (g *Game) reset() {
    72  	g.gopher.y = 0
    73  	g.gopher.v = 0
    74  	g.scroll.x = 0
    75  	g.scroll.v = initScrollV
    76  	for i := range g.groundY {
    77  		g.groundY[i] = initGroundY
    78  		g.groundTex[i] = randomGroundTexture()
    79  	}
    80  	g.gopher.atRest = false
    81  	g.gopher.flapped = false
    82  	g.gopher.dead = false
    83  	g.gopher.deadTime = 0
    84  }
    85  
    86  func (g *Game) Scene(eng sprite.Engine) *sprite.Node {
    87  	texs := loadTextures(eng)
    88  
    89  	scene := &sprite.Node{}
    90  	eng.Register(scene)
    91  	eng.SetTransform(scene, f32.Affine{
    92  		{1, 0, 0},
    93  		{0, 1, 0},
    94  	})
    95  
    96  	newNode := func(fn arrangerFunc) {
    97  		n := &sprite.Node{Arranger: arrangerFunc(fn)}
    98  		eng.Register(n)
    99  		scene.AppendChild(n)
   100  	}
   101  
   102  	// The ground.
   103  	for i := range g.groundY {
   104  		i := i
   105  		// The top of the ground.
   106  		newNode(func(eng sprite.Engine, n *sprite.Node, t clock.Time) {
   107  			eng.SetSubTex(n, texs[g.groundTex[i]])
   108  			eng.SetTransform(n, f32.Affine{
   109  				{tileWidth, 0, float32(i)*tileWidth - g.scroll.x},
   110  				{0, tileHeight, g.groundY[i]},
   111  			})
   112  		})
   113  		// The earth beneath.
   114  		newNode(func(eng sprite.Engine, n *sprite.Node, t clock.Time) {
   115  			eng.SetSubTex(n, texs[texEarth])
   116  			eng.SetTransform(n, f32.Affine{
   117  				{tileWidth, 0, float32(i)*tileWidth - g.scroll.x},
   118  				{0, tileHeight * tilesY, g.groundY[i] + tileHeight},
   119  			})
   120  		})
   121  	}
   122  
   123  	// The gopher.
   124  	newNode(func(eng sprite.Engine, n *sprite.Node, t clock.Time) {
   125  		a := f32.Affine{
   126  			{tileWidth * 2, 0, tileWidth*(gopherTile-1) + tileWidth/8},
   127  			{0, tileHeight * 2, g.gopher.y - tileHeight + tileHeight/4},
   128  		}
   129  		var x int
   130  		switch {
   131  		case g.gopher.dead:
   132  			x = frame(t, 16, texGopherDead1, texGopherDead2)
   133  			animateDeadGopher(&a, t-g.gopher.deadTime)
   134  		case g.gopher.v < 0:
   135  			x = frame(t, 4, texGopherFlap1, texGopherFlap2)
   136  		case g.gopher.atRest:
   137  			x = frame(t, 4, texGopherRun1, texGopherRun2)
   138  		default:
   139  			x = frame(t, 8, texGopherRun1, texGopherRun2)
   140  		}
   141  		eng.SetSubTex(n, texs[x])
   142  		eng.SetTransform(n, a)
   143  	})
   144  
   145  	return scene
   146  }
   147  
   148  // frame returns the frame for the given time t
   149  // when each frame is displayed for duration d.
   150  func frame(t, d clock.Time, frames ...int) int {
   151  	total := int(d) * len(frames)
   152  	return frames[(int(t)%total)/int(d)]
   153  }
   154  
   155  func animateDeadGopher(a *f32.Affine, t clock.Time) {
   156  	dt := float32(t)
   157  	a.Scale(a, 1+dt/20, 1+dt/20)
   158  	a.Translate(a, 0.5, 0.5)
   159  	a.Rotate(a, dt/math.Pi/-8)
   160  	a.Translate(a, -0.5, -0.5)
   161  }
   162  
   163  type arrangerFunc func(e sprite.Engine, n *sprite.Node, t clock.Time)
   164  
   165  func (a arrangerFunc) Arrange(e sprite.Engine, n *sprite.Node, t clock.Time) { a(e, n, t) }
   166  
   167  const (
   168  	texGopherRun1 = iota
   169  	texGopherRun2
   170  	texGopherFlap1
   171  	texGopherFlap2
   172  	texGopherDead1
   173  	texGopherDead2
   174  	texGround1
   175  	texGround2
   176  	texGround3
   177  	texGround4
   178  	texEarth
   179  )
   180  
   181  func randomGroundTexture() int {
   182  	return texGround1 + rand.Intn(4)
   183  }
   184  
   185  func loadTextures(eng sprite.Engine) []sprite.SubTex {
   186  	a, err := asset.Open("sprite.png")
   187  	if err != nil {
   188  		log.Fatal(err)
   189  	}
   190  	defer a.Close()
   191  
   192  	m, _, err := image.Decode(a)
   193  	if err != nil {
   194  		log.Fatal(err)
   195  	}
   196  	t, err := eng.LoadTexture(m)
   197  	if err != nil {
   198  		log.Fatal(err)
   199  	}
   200  
   201  	const n = 128
   202  	// The +1's and -1's in the rectangles below are to prevent colors from
   203  	// adjacent textures leaking into a given texture.
   204  	// See: http://stackoverflow.com/questions/19611745/opengl-black-lines-in-between-tiles
   205  	return []sprite.SubTex{
   206  		texGopherRun1:  sprite.SubTex{t, image.Rect(n*0+1, 0, n*1-1, n)},
   207  		texGopherRun2:  sprite.SubTex{t, image.Rect(n*1+1, 0, n*2-1, n)},
   208  		texGopherFlap1: sprite.SubTex{t, image.Rect(n*2+1, 0, n*3-1, n)},
   209  		texGopherFlap2: sprite.SubTex{t, image.Rect(n*3+1, 0, n*4-1, n)},
   210  		texGopherDead1: sprite.SubTex{t, image.Rect(n*4+1, 0, n*5-1, n)},
   211  		texGopherDead2: sprite.SubTex{t, image.Rect(n*5+1, 0, n*6-1, n)},
   212  		texGround1:     sprite.SubTex{t, image.Rect(n*6+1, 0, n*7-1, n)},
   213  		texGround2:     sprite.SubTex{t, image.Rect(n*7+1, 0, n*8-1, n)},
   214  		texGround3:     sprite.SubTex{t, image.Rect(n*8+1, 0, n*9-1, n)},
   215  		texGround4:     sprite.SubTex{t, image.Rect(n*9+1, 0, n*10-1, n)},
   216  		texEarth:       sprite.SubTex{t, image.Rect(n*10+1, 0, n*11-1, n)},
   217  	}
   218  }
   219  
   220  func (g *Game) Press(down bool) {
   221  	if g.gopher.dead {
   222  		// Player can't control a dead gopher.
   223  		return
   224  	}
   225  
   226  	if down {
   227  		switch {
   228  		case g.gopher.atRest:
   229  			// Gopher may jump from the ground.
   230  			g.gopher.v = jumpV
   231  		case !g.gopher.flapped:
   232  			// Gopher may flap once in mid-air.
   233  			g.gopher.flapped = true
   234  			g.gopher.v = flapV
   235  		}
   236  	} else {
   237  		// Stop gopher rising on button release.
   238  		if g.gopher.v < 0 {
   239  			g.gopher.v = 0
   240  		}
   241  	}
   242  }
   243  
   244  func (g *Game) Update(now clock.Time) {
   245  	if g.gopher.dead && now-g.gopher.deadTime > deadTimeBeforeReset {
   246  		// Restart if the gopher has been dead for a while.
   247  		g.reset()
   248  	}
   249  
   250  	// Compute game states up to now.
   251  	for ; g.lastCalc < now; g.lastCalc++ {
   252  		g.calcFrame()
   253  	}
   254  }
   255  
   256  func (g *Game) calcFrame() {
   257  	g.calcScroll()
   258  	g.calcGopher()
   259  }
   260  
   261  func (g *Game) calcScroll() {
   262  	// Compute velocity.
   263  	if g.gopher.dead {
   264  		// Decrease scroll speed when the gopher dies.
   265  		g.scroll.v += deadScrollA
   266  		if g.scroll.v < 0 {
   267  			g.scroll.v = 0
   268  		}
   269  	} else {
   270  		// Increase scroll speed.
   271  		g.scroll.v += scrollA
   272  	}
   273  
   274  	// Compute offset.
   275  	g.scroll.x += g.scroll.v
   276  
   277  	// Create new ground tiles if we need to.
   278  	for g.scroll.x > tileWidth {
   279  		g.newGroundTile()
   280  
   281  		// Check whether the gopher has crashed.
   282  		// Do this for each new ground tile so that when the scroll
   283  		// velocity is >tileWidth/frame it can't pass through the ground.
   284  		if !g.gopher.dead && g.gopherCrashed() {
   285  			g.killGopher()
   286  		}
   287  	}
   288  }
   289  
   290  func (g *Game) calcGopher() {
   291  	// Compute velocity.
   292  	g.gopher.v += gravity
   293  
   294  	// Compute offset.
   295  	g.gopher.y += g.gopher.v
   296  
   297  	g.clampToGround()
   298  }
   299  
   300  func (g *Game) newGroundTile() {
   301  	// Compute next ground y-offset.
   302  	next := g.nextGroundY()
   303  	nextTex := randomGroundTexture()
   304  
   305  	// Shift ground tiles to the left.
   306  	g.scroll.x -= tileWidth
   307  	copy(g.groundY[:], g.groundY[1:])
   308  	copy(g.groundTex[:], g.groundTex[1:])
   309  	last := len(g.groundY) - 1
   310  	g.groundY[last] = next
   311  	g.groundTex[last] = nextTex
   312  }
   313  
   314  func (g *Game) nextGroundY() float32 {
   315  	prev := g.groundY[len(g.groundY)-1]
   316  	if change := rand.Intn(groundChangeProb) == 0; change {
   317  		return (groundMax-groundMin)*rand.Float32() + groundMin
   318  	}
   319  	if wobble := rand.Intn(groundWobbleProb) == 0; wobble {
   320  		return prev + (rand.Float32()-0.5)*climbGrace
   321  	}
   322  	return prev
   323  }
   324  
   325  func (g *Game) gopherCrashed() bool {
   326  	return g.gopher.y+tileHeight-climbGrace > g.groundY[gopherTile+1]
   327  }
   328  
   329  func (g *Game) killGopher() {
   330  	g.gopher.dead = true
   331  	g.gopher.deadTime = g.lastCalc
   332  	g.gopher.v = jumpV * 1.5 // Bounce off screen.
   333  }
   334  
   335  func (g *Game) clampToGround() {
   336  	if g.gopher.dead {
   337  		// Allow the gopher to fall through ground when dead.
   338  		return
   339  	}
   340  
   341  	// Compute the minimum offset of the ground beneath the gopher.
   342  	minY := g.groundY[gopherTile]
   343  	if y := g.groundY[gopherTile+1]; y < minY {
   344  		minY = y
   345  	}
   346  
   347  	// Prevent the gopher from falling through the ground.
   348  	maxGopherY := minY - tileHeight
   349  	g.gopher.atRest = false
   350  	if g.gopher.y >= maxGopherY {
   351  		g.gopher.v = 0
   352  		g.gopher.y = maxGopherY
   353  		g.gopher.atRest = true
   354  		g.gopher.flapped = false
   355  	}
   356  }