Create a Flappy Bird Clone With Golang P2

Posted in golang tutorials -

Last time, we stopped at mentioning that there are three functions needed for the game loop: Update(), Draw() and Layout(), of which all are functions of the Game struct. According to the cheatsheet, the whole Game struct is an instance of the Game interface, as follow:

type Game interface {
    // Update updates a game by one tick. The given argument represents a screen image.
    Update(screen *Image) error

    // Draw draw the game screen. The given argument represents a screen image.
    //
    // (To be exact, Draw is not defined in this interface due to backward compatibility, but RunGame's
    // behavior depends on the existence of Draw.)
    Draw(screen *Image)

    // Layout accepts a native outside size in device-independent pixels and returns the game's logical
    // screen size. On desktops, the outside is a window or a monitor (fullscreen mode)
    //
    // Even though the outside size and the screen size differ, the rendering scale is automatically
    // adjusted to fit with the outside.
    //
    // You can return a fixed screen size if you don't care, or you can also return a calculated screen
    // size adjusted with the given outside size.
    Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
}

If you have a background of C, C++, Java and the like, concepts of interface (and struct) might be familiar to you, but in case they aren’t, here are my quick explanations:

  • struct is the “equivalent” of class in Go. By definition, it is simply a combination of several variables, but it can be made to be class-like structure by having functions. Go functions have an optional type (define after the keyword func), which makes the func a method of that type. By providing a a pointer to the struct (denoted by an asterisk (*) ) as the type, we can make the function affect the instance of the struct (instead of the struct type itself), hence achieve similar behaviors as classes’ private functions in other OOP languages.
  • interface is a kind of abstraction for class, which only lists required functions whose bodies are empty. The users of interfaces are required to implement it by creating a class that has all of required functions. In this case, since the ebiten.RunGame function uses all three functions Update(), Draw() and Layout(), the Game struct is required to have all of those functions, but since Game struct implements Game interface, that requirement must have had satisfied.

You may also find the following overview sheet handy to look at from time to time:

Now let’s examine the Game class on the example we copied earlier:

type Game struct {
	count int
}

func (g *Game) Update(screen *ebiten.Image) error {
	g.count++
	return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
	op := &ebiten.DrawImageOptions{}
	op.GeoM.Translate(-float64(frameWidth)/2, -float64(frameHeight)/2)
	op.GeoM.Translate(screenWidth/2, screenHeight/2)
	i := (g.count / 5) % frameNum
	sx, sy := frameOX+i*frameWidth, frameOY
	screen.DrawImage(runnerImage.SubImage(image.Rect(sx, sy, sx+frameWidth, sy+frameHeight)).(*ebiten.Image), op)
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
	return screenWidth, screenHeight
}

Some notices:

  • The Game struct has one attribute of type integer called count. This attribute is increased by 1 in every update.
  • The Layout() seems to do nothing other than returning the screenWidth and screenHeight.
  • The count attribute is used in calculating i using modula operator. It means that i will rotate from 0 to frameNum-1, which is 7 (frameNum is defined in the const section).
  • The i is used in calculating sx as sx := frameOX+i*frameWidth. With frameOX=0 and frameWidth=32 (also in const section), sx will vary from 0*32 to 7*32. sy is fixed at 0.
  • image.Rect defines a Rectangle area with (sx, sy) as the top left and (sx+frameWidth, sy+frameHeight) as the bottom right. Considering that frameWidth and frameHeight both equal to 32, it means we achieve a 32x32 rectangle.

For better understanding, let’s print i, sx, sy, sx+frameWidth, sy+frameHeight to the screen.

func (g *Game) Draw(screen *ebiten.Image) {
    ...
    println(i, sx, sy, sx+frameWidth, sy+frameHeight)
	screen.DrawImage(runnerImage.SubImage(image.Rect(sx, sy, sx+frameWidth, sy+frameHeight)).(*ebiten.Image), op)
}

Here we use println instead of print, since we want a new line break in after each print. The result is the following sequence repeating over and over:

0 0 32 32 64
0 0 32 32 64
0 0 32 32 64
0 0 32 32 64
0 0 32 32 64
1 32 32 64 64
1 32 32 64 64
1 32 32 64 64
1 32 32 64 64
1 32 32 64 64
2 64 32 96 64
2 64 32 96 64
2 64 32 96 64
2 64 32 96 64
2 64 32 96 64
3 96 32 128 64
3 96 32 128 64
3 96 32 128 64
3 96 32 128 64
3 96 32 128 64
4 128 32 160 64
4 128 32 160 64
4 128 32 160 64
4 128 32 160 64
4 128 32 160 64
5 160 32 192 64
5 160 32 192 64
5 160 32 192 64
5 160 32 192 64
5 160 32 192 64
6 192 32 224 64
6 192 32 224 64
6 192 32 224 64
6 192 32 224 64
6 192 32 224 64
7 224 32 256 64
7 224 32 256 64
7 224 32 256 64
7 224 32 256 64
7 224 32 256 64

Notice that each combination is repeated 5 times before moving to the next. It was caused by the (g.count)/5 part, which only takes the quotient, which causes i to repeat 5 times before changing. We can verify that by making this change:

i := g.count % frameNum

Try making that change and see its affect on the animation.

Now that we understand what the image.Rect() gives us, let’s have a look at the runnerImage:

runnerImage

runnerImage

As you can see, it consists of three lines of image, each of the lines, in turn consists of several sequential square image of the same guy with different postures. The SubImage function returns one of these small squared images (the .(*ebiten.Image) simply casts whatever returned by SubImage to the *ebiten.Image type, which is required by DrawImage().

Since sy is fixed at 32, we seem to only take images from the second row of the aforementioned image. Could you change the animation from running guy (second row) to dancing guy (first row), or standing guy (third row)? (Hint: Change the sy, and also the frameNum).

We only have these lines of code left to figure out:

	op := &ebiten.DrawImageOptions{}
	op.GeoM.Translate(-float64(frameWidth)/2, -float64(frameHeight)/2)
	op.GeoM.Translate(screenWidth/2, screenHeight/2)

Could you look them up from the Cheatsheet and see what is the role of those lines? Try changing the parameters, and/or removing one of the lines to see what happens.

In case you have issues figuring out, don’t worry. We’ll cover that in the next post.

Written by Huy Mai