Watch the associated video on YouTube or MakerTube

Introduction #
In this tutorial we will expand our Ebitengine game to include moving obstacles in the form of chopsticks.
The code for this tutorial is available here. Please consider donating if you find this tutorial helpful.
Add a Chopstick Type #
We will create a chopstick type with:
- An X and Y position
- A flag controlling whether the chopstick image is flipped vertically
// chopstickGap is the minimum gap between the tip of a chopstick and the top
// or bottom edge of the screen.
const chopstickGap = 36
// chopstick represents a chopstick obstacle.
type chopstick struct {
x, y int
flip bool
}
// randomizeY randomizes the Y position of a chopstick.
func (c *chopstick) randomizeY() {
c.y = chopstickGap + rand.Intn(screenH-chopstickGap*2)
}
Randomize Placement #
We will create four instances of chopsticks. By default, all of our chopsticks are located at (0, 0).
Here we create a horizontal gap between the chopsticks by setting their initial X positions. Because we will move all four chopsticks across the screen at the same speed, the gap between them will not change.
To make the game more challenging, we will randomize the chopsticks Y placement. This will occur when the game first starts, and when the chopstick moves out of view beyond the screen.
Each chopstick will alternate in pointing up from the bottom of the screen or down from the top of the screen.
// reset game state.
func (g *game) reset() {
// ...
// Clear chopsticks.
g.chopsticks = g.chopsticks[:0]
// Add chopsticks and randomize placement.
defaultFlip := rand.Intn(2) == 0
for i := 0; i < 4; i++ {
c := &chopstick{
x: i*screenW/4 + 42,
flip: (i%2 == 0) == defaultFlip,
}
c.randomizeY()
g.chopsticks = append(g.chopsticks, c)
}
}
The defaultFlip
variable specifies whether the first chopstick on the screen is flipped.
rand.Intn(2)
will return either zero or one. When its value is zero, we will flip the first chopstick vertically.
i%2
calculates the modulo, or remainder of i/2
. For example, 0%2=0
, 1%2=1
, 2%2=0
, etc.
Move the Chopsticks #
We could move the player across the screen by updating their X position. We would then need to re-center the camera.
In this case, it’s easier to move the world around the player, so we will move the chopsticks left across the screen.
When a chopstick moves out of view beyond the left edge, we will move it to the right edge and randomize its Y position.
// Update is where we update the game state.
func (g *game) Update() error {
// ...
// Move chopsticks on the first tick and every 3rd tick thereafter.
if g.tick%3 == 0 {
for _, c := range g.chopsticks {
c.x--
if c.x == -16 {
c.x = screenW - 1
c.randomizeY()
}
}
}
}
Handle Chopstick Collisions #
Each tick, we will check for a collision between the player and the chopsticks.
To simplify collision detection, we will define collision rectangles for the player and each of the obstacles.
The chopsticks are (mostly) rectangular in shape, therefore each chopstick will have one collision rectangle:
// collisionRect returns the collision rect of a chopstick.
func (c *chopstick) collisionRect() image.Rectangle {
// Calculate sign (1 or -1) based on the flip attribute.
flipSign := 1
if c.flip {
flipSign = -1
}
// Calculate rect, applying sign value to Y direction.
return image.Rect(c.x+5, c.y, c.x+11, c.y+270*flipSign)
}
The character, however, is not a rectangular shape, so we will define two collision rectangles to the player.
Here we the define collision rectangle of the tail (vertical segment) and the body (horizontal segment):
// playerTailRect returns the collision rect of the player character's tail.
func (g *game) playerTailRect() image.Rectangle {
return image.Rect(int(g.playerX+14), int(g.playerY+2), int(g.playerX+16), int(g.playerY+10))
}
// playerBodyRect returns the collision rect of the player character's body.
func (g *game) playerBodyRect() image.Rectangle {
return image.Rect(int(g.playerX), int(g.playerY+10), int(g.playerX+14), int(g.playerY+15))
}
To check for a collision between two rectangles, we define the following function:
// rectsCollide returns whether two rectangles collide.
func rectsCollide(r1 image.Rectangle, r2 image.Rectangle) bool {
return r1.Min.X < r2.Max.X && // r1 left before r2 right
r1.Max.X > r2.Min.X && // r1 right after r2 left
r1.Min.Y < r2.Max.Y && // r1 top before r2 bottom
r1.Max.Y > r2.Min.Y // r1 bottom after r2 top
}
We can then reset the game whenever the player collides with a chopstick:
// Update is where we update the game state.
func (g *game) Update() error {
// ...
// Handle collision with chopsticks.
tailRect := g.playerTailRect()
bodyRect := g.playerBodyRect()
for _, c := range g.chopsticks {
r := c.collisionRect()
if rectsCollide(r, tailRect) || rectsCollide(r, bodyRect) {
g.reset()
return nil
}
}
}
Load the Chopstick Image #
During initialization, we load an embedded 16x270 image of a chopstick pointing up:
// initialize sets up the initial state of the game.
func (g *game) initialize() {
// ...
// Load chopstick image.
g.chopstickImg = loadImage("asset/image/chopstick.png")
}
Draw the Chopsticks #
To draw the chopsticks onto the screen, we will iterate over each of them and modify the GeoM provided to Ebitengine.
When flip
is enabled, we flip the geometry matrix vertically, and a chopstick pointing down is drawn onto the screen.
Note how we do not need to adjust the Y position when flipping, because the image is flipped across its Y origin axis (top).
This causes the resulting flipped image to be entirely off-screen. We must move the image downward to make it visible.
// Draw chopsticks.
for _, c := range g.chopsticks {
op := &ebiten.DrawImageOptions{}
if c.flip {
op.GeoM.Scale(1, -1)
}
op.GeoM.Translate(float64(c.x), float64(c.y))
screen.DrawImage(g.chopstickImg, op)
}
Stay tuned for the next tutorial, Adding Text to Your Ebitengine Game.
Please consider donating if you found this tutorial helpful.