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.RunGamefunction uses all three functionsUpdate(),Draw()andLayout(), theGamestruct is required to have all of those functions, but sinceGamestruct implementsGameinterface, 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
Gamestruct has one attribute of type integer calledcount. This attribute is increased by 1 in every update. - The
Layout()seems to do nothing other than returning the screenWidth and screenHeight. - The
countattribute is used in calculatingiusing modula operator. It means thatiwill rotate from0toframeNum-1, which is7(frameNumis defined in theconstsection). - The
iis used in calculatingsxassx := frameOX+i*frameWidth. WithframeOX=0andframeWidth=32(also inconstsection),sxwill vary from0*32to7*32.syis fixed at0. - image.Rect defines a Rectangle area with
(sx, sy)as the top left and(sx+frameWidth, sy+frameHeight)as the bottom right. Considering thatframeWidthandframeHeightboth equal to32, it means we achieve a32x32rectangle.
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
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
syis fixed at32, 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 thesy, and also theframeNum).
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.
