Разработка простой игры на 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
У вас откроется окно, и белый квадратик можно двигать стрелками.
- Что отработали
✔ игровой цикл через 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 может быть отличным языком для простых и кроссплатформенных игр. А самое главное — вы видите результат за минуты, не теряя мощи языка.



