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