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 functionsUpdate()
,Draw()
andLayout()
, theGame
struct is required to have all of those functions, but sinceGame
struct implementsGame
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 calledcount
. 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 calculatingi
using modula operator. It means thati
will rotate from0
toframeNum-1
, which is7
(frameNum
is defined in theconst
section). - The
i
is used in calculatingsx
assx := frameOX+i*frameWidth
. WithframeOX=0
andframeWidth=32
(also inconst
section),sx
will vary from0*32
to7*32
.sy
is 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 thatframeWidth
andframeHeight
both equal to32
, it means we achieve a32x32
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
:
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 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.