Game Development 7 min read

Building a Tank Battle Game in Go with Ebiten

This article walks through creating a tank‑battle game in Go using the Ebiten engine, covering repository setup, required tools, code organization, the core Update and Draw loops, and the mathematical formulas—rotation matrix, cross product, dot product, and the separating axis theorem—used for collision detection.

Nullbody Notes
Nullbody Notes
Nullbody Notes
Building a Tank Battle Game in Go with Ebiten

Project Repository

Repository: https://github.com/gofish2020/tankgame. After cloning, run go run main.go to start the game.

Engine

Implemented with Ebitengine. A collection of examples for learning is available at https://ebiten-zh.vercel.app/examples/.

Development Environment

Go version: go1.20 Linux or macOS requires a C compiler; installation instructions are at https://ebiten-zh.vercel.app/documents/install.html

Code Structure

tankgame
├── go.mod
├── go.sum
├── main.go            // entry point
├── package
│   ├── game
│   │   └── game.go    // global parameters and game‑progress logic
│   ├── monitor
│   │   └── screen.go   // screen width/height
│   ├── tank
│   │   ├── barrier.go // obstacle creation & drawing
│   │   ├── check.go   // collision detection
│   │   ├── gameover.go// game‑over screen
│   │   ├── keyboard.go// tank‑surrounding text drawing
│   │   ├── menu.go    // start menu
│   │   ├── npcmove.go // NPC movement & enemy search
│   │   ├── name.go    // tank name handling
│   │   └── tank.go    // tank drawing & logic
│   └── utils
│       ├── sound      // audio handling
│       ├── utils.go   // sector drawing
│       └── variable.go// global parameters controlling game progress
├── resource           // image assets

Update Loop

// Update data
func (g *Game) Update() error {
    enemyCount := 0
    // Restart game
    g.Restart()
    // Play background music
    sound.PlayBGM()
    // Separate player and NPC tanks
    var playerPosition tank.TankPosition
    var npcPositions []tank.TankPosition
    // Detect living tanks
    liveTanks := []*tank.Tank{}
    for _, tk := range g.tks {
        // Update tank
        tk.Update()
        // Collision detection
        tk.CheckCollisions(g.tks, g.barriers)
        // Limit tank movement range
        tk.LimitTankRange(tank.MinXCoordinates, tank.MinYCoordinates, float64(monitor.ScreenWidth)-30, float64(monitor.ScreenHeight)-30)
        // Record tank position
        if tk.TkType == tank.TankTypePlayer {
            playerPosition.X = tk.X
            playerPosition.Y = tk.Y
            playerPosition.TK = tk
            if tk.HealthPoints == 0 {
                tank.UpdateNameList(tk.Name)
                utils.GameProgress = "over"
                sound.PlaySound("yiwai")
                break
            }
        } else {
            // Record NPC position
            if tk.HealthPoints == 0 {
                tank.UpdateNameList(tk.Name)
                utils.KilledCount++
                tk.DeathSound()
            } else {
                enemyCount++
                npcPositions = append(npcPositions, tank.TankPosition{X: tk.X, Y: tk.Y, TK: tk})
            }
        }
        if tk.HealthPoints != 0 {
            liveTanks = append(liveTanks, tk)
        }
    }
    // Update remaining tanks
    g.tks = liveTanks
    // Initial screen
    if utils.GameProgress == "init" || utils.GameProgress == "pass" {
        tank.MenuUpdate(g.tks) // button movement + bullet collision
    } else if utils.GameProgress == "play" {
        // Move NPC tanks and detect enemies in attack range
        tank.MoveAndFineEnemyTank(playerPosition, npcPositions)
    }
    if utils.GameProgress == "play" && enemyCount == 0 {
        utils.GameProgress = "next" // next level
    }
    // Game over key handling
    tank.GameOverUpdate()
    return nil
}

Draw Loop

// Render screen
func (g *Game) Draw(screen *ebiten.Image) {
    screen.Fill(color.RGBA{240, 222, 180, 215})
    if utils.GameProgress == "init" || utils.GameProgress == "pass" {
        // Start screen
        tank.MenuDraw(screen)
    }
    x, y := 0.0, 0.0
    // Draw each tank
    for _, tk := range g.tks {
        tk.Draw(screen)
        // Draw key overlay for player tank
        if tk.TkType == tank.TankTypePlayer {
            tank.KeyPressDrawAroundTank(tk, screen)
            x, y = tk.X, tk.Y // player perspective
        }
    }
    // Draw fog of war and obstacles
    if utils.GameProgress == "play" {
        tank.DrawWarFogAndBarriers(screen, x, y, g.barriers)
    }
    // Draw death list
    tank.DrawNameList(screen)
    // Game over screen
    tank.GameOverDraw(screen)
}

Mathematical Foundations for Collision Detection

Rotation matrix – computes new coordinates after rotating a point.

Cross‑product formula – determines whether a point lies inside a polygon; the coordinate system uses Y increasing downward, reversing the usual left‑hand (counter‑clockwise) rule.

Dot‑product formula – used for vector projection calculations.

Separating Axis Theorem (SAT) – tests rectangle intersection by projecting polygon vertices onto each edge normal and checking for overlap; any axis without overlap indicates no intersection.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GoGame DevelopmentCollision DetectionTank GameEbiten
Nullbody Notes
Written by

Nullbody Notes

Go backend development, learning open-source project source code together, focusing on simplicity and practicality.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.