Chapter 1: Game State and Engine
July 14, 2025 ยท View on GitHub
Welcome to your first step in understanding how the Snake game works! Every game, no matter how simple or complex, needs a central place to keep track of everything that's happening. Think of it as the game's brain or the conductor of an orchestra. In our Snake game, this central hub is represented by the Game struct.
This Game struct holds all the Game State โ things like where the snake is, where the food is, what your score is, and if you've lost or paused the game. It also works closely with the Game Engine (which is Ebitengine in our case) to run the show.
The main job of the Game struct, working with the engine, is to constantly repeat two main actions:
- Update: Check for player input (like pressing arrow keys), move things around based on time, check for collisions, and change the game state accordingly.
- Draw: Take the current game state and draw everything onto the screen so you can see it.
This constant cycle of Updating and Drawing is the core Game Loop.
The Game's Brain: The Game Struct
Let's look at the Game struct in the code (main.go). It's like a container holding all the important pieces of information about the game at any given moment.
type Game struct {
snake []Point // Where the snake's body parts are
direction Point // Which way the snake is moving
lastUpdate time.Time // When the snake last moved
food *Point // Where the food is
// ... other game state variables
gameOver bool // Is the game over?
paused bool // Is the game paused?
score int // The player's score
// ... other components like particles, sound, buttons
}
This struct holds the state of the game. When you start the game, all these variables are set to their initial values (like the snake starting in the middle). As you play, these values change (snake moves, food is eaten, score goes up, game over becomes true).
The Game Conductor: Ebitengine and the ebiten.Game Interface
How does our Game struct actually do things? This is where the Ebitengine Game Engine comes in. Ebitengine provides the main loop and timing. It expects our game to follow a specific set of rules, defined by the ebiten.Game interface.
An interface in Go is like a contract. If a type (like our Game struct) wants to work with something else (like ebiten.RunGame), it must provide specific methods listed in the interface. The ebiten.Game interface requires three methods:
Update(): This method is called repeatedly by the engine, usually 60 times per second (frames per second or FPS). This is where you put all your game logic that needs to happen every frame.Draw(screen *ebiten.Image): This method is called by the engine right afterUpdate(if needed). This is where you draw everything onto the screen image that Ebitengine will display.Layout(outsideWidth, outsideHeight int): This method tells Ebitengine the logical screen size your game uses, regardless of the actual window size.
Our Game struct implements this interface, meaning it has Update, Draw, and Layout methods defined. This is how the Ebitengine engine interacts with our game's brain.
How the Engine Starts the Game Loop
Let's look at the very end of the main.go file, in the main function:
func main() {
ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Snake!")
g := NewGame() // Create a new Game instance
if err := ebiten.RunGame(g); err != nil { // Start the Ebitengine game loop
log.Fatal(err)
}
}
This is the entry point of our program.
- We set the window size and title.
- We create a new instance of our
Gamestruct usingNewGame(). - We call
ebiten.RunGame(g). This is the magic line! We pass ourGameinstancegto Ebitengine. Ebitengine then takes over and starts the main game loop, callingg.Update()andg.Draw()repeatedly.
The Engine's Routine: Update and Draw
Imagine the Ebitengine engine as a strict clock ticking many times per second. Every time it ticks, it performs a simple sequence:
sequenceDiagram
participant E as Ebitengine Engine
participant G as Our Game (Game struct)
E->>G: Call Update()
Note over G: Handle input, move snake,<br/>check collisions, update score/state...
E->>G: Call Draw(screen)
Note over G: Draw food, draw snake,<br/>draw score, draw game over text...
E-->>E: Display screen to player
Note over E: Repeat this cycle ~60 times/sec
Let's look at simplified versions of the Update and Draw methods from the Game struct:
// Update is called every game tick (frame).
func (g *Game) Update() error {
// ... handle input (arrow keys, pause, etc.)
if g.paused {
return nil // Do nothing if paused
}
if g.gameOver {
// Check for restart input if game over
return nil
}
// Check if enough time has passed to move the snake
if time.Since(g.lastUpdate) >= gameSpeed {
// ... move the snake
// ... check for collisions
// ... check for food
g.lastUpdate = time.Now() // Record when we last moved
}
// ... update other game elements like particles, buttons
return nil
}
The Update method is where all the game's logic happens. It checks if the game is over or paused, handles player input (like changing direction), checks if it's time to move the snake based on gameSpeed and lastUpdate, and then performs actions like checking for collisions or food.
// Draw is called every game tick (frame).
func (g *Game) Draw(screen *ebiten.Image) {
// screen is the image Ebitengine provides to draw on
// ... draw border
// ... draw score
// ... draw buttons
if g.food != nil {
// ... draw the food block
}
// ... draw the snake body
if g.gameOver || g.paused {
// ... draw "Game Over" or "Game Paused" text
}
// ... draw particles
}
The Draw method is responsible for visualizing the game state. It takes the screen image and uses Ebitengine's drawing functions (vector.DrawFilledRect, text.Draw, etc.) to draw the snake, food, score, buttons, and any status messages based on the current values stored in the Game struct.
The Layout method is simpler:
// Layout is called when the window size changes.
// It tells Ebitengine our game's logical screen size.
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return 640, 480 // We want our game to render at 640x480
}
This simply tells Ebitengine that even if the window is resized, the game should render as if the screen is 640 pixels wide and 480 pixels tall. Ebitengine will scale the drawing appropriately.
In Summary
The Game struct is the central brain, holding the state of everything in the game. It works hand-in-hand with the Ebitengine engine. The engine constantly calls the Update method (to change the state based on time and input) and the Draw method (to show the state on the screen). This continuous cycle drives the entire game.
Now that we understand the central hub of the game, we can start looking at the individual components it manages!
References: [1]