Разработка простой игры на Go: практический разбор

Go чаще ассоциируется с сервисами, API и бэкендом, но на нём вполне можно писать игры — особенно 2D.
В этом материале мы создадим минимальную игру с управлением персонажем на экране, используя популярную библиотеку Ebiten.

👣 Golang Go – авторский канал, посвященный Go разработке, Devops и созданию высоконагруженных сервисов.

https://t.me/addlist/MUtJEeJSxeY2YTFi – целая папка полезных ресурсов.

Поехали!

1. Что будем делать

Мы реализуем:

  • окно
  • спрайт
  • управление через клавиатуру
  • ограничение движения по границам

Игра: квадрат, которым можно двигать по экрану стрелками.


2. Установка Ebiten


go get github.com/hajimehoshi/ebiten/v2

3. Структура игры

Ebiten ожидает тип, реализующий интерфейс ebiten.Game, то есть методы:

Update() error
Draw(screen *ebiten.Image)
Layout(w, h int)(int, int)

4. Практический код

Создаём файл main.go:

package main

import (
    "log"

    "github.com/hajimehoshi/ebiten/v2"
    "github.com/hajimehoshi/ebiten/v2/ebitenutil"
)

type Game struct {
    x, y float64
}

func (g *Game) Update() error {
    const speed = 2

    if ebiten.IsKeyPressed(ebiten.KeyRight) {
        g.x += speed
    }
    if ebiten.IsKeyPressed(ebiten.KeyLeft) {
        g.x -= speed
    }
    if ebiten.IsKeyPressed(ebiten.KeyUp) {
        g.y -= speed
    }
    if ebiten.IsKeyPressed(ebiten.KeyDown) {
        g.y += speed
    }

    // ограничиваем движение по экрану
    if g.x < 0 { g.x = 0 }
    if g.y < 0 { g.y = 0 }
    if g.x > 320 { g.x = 320 }
    if g.y > 240 { g.y = 240 }

    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    ebitenutil.DrawRect(screen, g.x, g.y, 20, 20, ebiten.ColorM{})
}

func (g *Game) Layout(w, h int) (int, int) {
    return 640, 480
}

func main() {
    g := &Game{x: 100, y: 100}
    ebiten.SetWindowSize(640, 480)
    ebiten.SetWindowTitle("Go Game Example")

    if err := ebiten.RunGame(g); err != nil {
        log.Fatal(err)
    }
}

5. Запускаем игру

go run main.go

У вас откроется окно, и белый квадратик можно двигать стрелками.

  1. Что отработали
    ✔ игровой цикл через Update
    ✔ отрисовка через Draw
    ✔ обработка ввода
    ✔ базовая физика — движение и ограничения

Что отработали

Теперь вы можете расширять игру:

  • загрузка текстур (спрайты)
  • анимация движения
  • столкновения и логика
  • фон и UI
  • звуки

Ebiten отлично документирован и поддерживает HTML5, Windows, Linux, macOS и мобильные платформы.

Разовьем игру на Go дальше и сделаем из двигающегося квадрата мини аркаду: игрок уклоняется от падающих врагов, набирает очки, при столкновении игра перезапускается.

Что добавим:

  • враги, которые падают сверху
  • управление игроком стрелками
  • счет
  • простая система столкновений
  • рестарт игры после проигрыша

Ниже полный пример на Go с использованием Ebiten.

package main

import (
"image/color"
"log"
"math/rand"
"time"

"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)

const (
screenWidth = 640
screenHeight = 480

playerWidth = 30
playerHeight = 30
playerSpeed = 4

enemyWidth = 20
enemyHeight = 20
enemyMinSpeed = 2
enemyMaxSpeed = 5

maxEnemies = 10
)

type Enemy struct {
x, y float64
speed float64
alive bool
}

type Game struct {
playerX float64
playerY float64

enemies []Enemy
score int
gameOver bool
}

func NewGame() *Game {
g := &Game{
playerX: float64(screenWidth/2 - playerWidth/2),
playerY: float64(screenHeight - playerHeight - 10),
enemies: make([]Enemy, maxEnemies),
score: 0,
gameOver: false,
}
g.initEnemies()
return g
}

func (g *Game) initEnemies() {
rand.Seed(time.Now().UnixNano())
for i := 0; i < maxEnemies; i++ {
g.spawnEnemy(i)
}
}

func (g *Game) spawnEnemy(i int) {
x := rand.Intn(screenWidth - enemyWidth)
y := rand.Intn(200) * -1 // чуть выше экрана
speed := float64(enemyMinSpeed + rand.Intn(enemyMaxSpeed-enemyMinSpeed+1))
g.enemies[i] = Enemy{
x: float64(x),
y: float64(y),
speed: speed,
alive: true,
}
}

// Update - игровая логика
func (g *Game) Update() error {
if g.gameOver {
// простейший рестарт по нажатию пробела
if ebiten.IsKeyPressed(ebiten.KeySpace) {
g.reset()
}
return nil
}

g.updatePlayer()
g.updateEnemies()
g.checkCollisions()
return nil
}

func (g *Game) reset() {
g.playerX = float64(screenWidth/2 - playerWidth/2)
g.playerY = float64(screenHeight - playerHeight - 10)
g.score = 0
g.gameOver = false
g.initEnemies()
}

func (g *Game) updatePlayer() {
if ebiten.IsKeyPressed(ebiten.KeyLeft) {
g.playerX -= playerSpeed
}
if ebiten.IsKeyPressed(ebiten.KeyRight) {
g.playerX += playerSpeed
}
if ebiten.IsKeyPressed(ebiten.KeyUp) {
g.playerY -= playerSpeed
}
if ebiten.IsKeyPressed(ebiten.KeyDown) {
g.playerY += playerSpeed
}

// границы экрана
if g.playerX < 0 {
g.playerX = 0
}
if g.playerX > float64(screenWidth-playerWidth) {
g.playerX = float64(screenWidth - playerWidth)
}
if g.playerY < 0 {
g.playerY = 0
}
if g.playerY > float64(screenHeight-playerHeight) {
g.playerY = float64(screenHeight - playerHeight)
}
}

func (g *Game) updateEnemies() {
for i := range g.enemies {
if !g.enemies[i].alive {
continue
}
g.enemies[i].y += g.enemies[i].speed

// как только враг ушел за нижнюю границу, считаем очко и спавним его заново
if g.enemies[i].y > screenHeight {
g.score++
g.spawnEnemy(i)
}
}
}

func rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh float64) bool {
return ax < bx+bw &&
ax+aw > bx &&
ay < by+bh &&
ay+ah > by
}

func (g *Game) checkCollisions() {
for i := range g.enemies {
if !g.enemies[i].alive {
continue
}
if rectsOverlap(
g.playerX, g.playerY, playerWidth, playerHeight,
g.enemies[i].x, g.enemies[i].y, enemyWidth, enemyHeight,
) {
g.gameOver = true
return
}
}
}

// Draw - отрисовка кадра
func (g *Game) Draw(screen *ebiten.Image) {
// фон
screen.Fill(color.RGBA{20, 20, 40, 255})

// игрок
ebitenutil.DrawRect(screen, g.playerX, g.playerY, playerWidth, playerHeight, color.RGBA{50, 200, 50, 255})

// враги
for _, e := range g.enemies {
if !e.alive {
continue
}
ebitenutil.DrawRect(screen, e.x, e.y, enemyWidth, enemyHeight, color.RGBA{220, 60, 60, 255})
}

// текст со счетом
ebitenutil.DebugPrint(screen,
"Use arrows to move. Avoid enemies.\nScore: "+
itoa(g.score)+"\nPress SPACE to restart after game over.",
)

if g.gameOver {
ebitenutil.DebugPrint(screen, "GAME OVER\nPress SPACE to restart")
}
}

// простая функция перевода int в string без strconv для компактности
func itoa(n int) string {
if n == 0 {
return "0"
}
neg := false
if n < 0 {
neg = true
n = -n
}
buf := [20]byte{}
i := len(buf)
for n > 0 {
i--
buf[i] = byte('0' + n%10)
n /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}

// Layout - логический размер экрана
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenWidth, screenHeight
}

func main() {
game := NewGame()

ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Go mini game example")

if err := ebiten.RunGame(game); err != nil {
log.Fatal(err)
} }

🎯 Итог: Go может быть отличным языком для простых и кроссплатформенных игр. А самое главное — вы видите результат за минуты, не теряя мощи языка.

+1
0
+1
2
+1
0
+1
0
+1
1

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *