This repository is learn how to use ebiten to build a snake game
package internal
import (
"image/color"
"math/rand"
"time"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/text/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
)
var (
dirUp = Point{x: 0, y: -1}
dirDown = Point{x: 0, y: 1}
dirLeft = Point{x: -1, y: 0}
dirRight = Point{x: 1, y: 0}
MplusFaceSource *text.GoTextFaceSource
)
const (
gameSpeed = time.Second / 6
ScreenWidth = 640
ScreenHeight = 480
gridSize = 20
)
type Point struct {
x, y int
}
type Game struct {
snake []Point
direction Point
lastUpdate time.Time
food Point
randGenerator *rand.Rand
gameOver bool
}
func (g *Game) Update() error {
if g.gameOver {
return nil
}
// handle key
if ebiten.IsKeyPressed(ebiten.KeyW) {
g.direction = dirUp
} else if ebiten.IsKeyPressed(ebiten.KeyS) {
g.direction = dirDown
} else if ebiten.IsKeyPressed(ebiten.KeyA) {
g.direction = dirLeft
} else if ebiten.IsKeyPressed(ebiten.KeyD) {
g.direction = dirRight
}
// slow down
if time.Since(g.lastUpdate) < gameSpeed {
return nil
}
g.lastUpdate = time.Now()
g.updateSnake(&g.snake, g.direction)
return nil
}
// isBadCollision - check if snake is collision
func (g Game) isBadCollision(
newHead Point,
snake []Point,
) bool {
// check if out of bound
if newHead.x < 0 || newHead.y < 0 ||
newHead.x >= ScreenWidth/gridSize || newHead.y >= ScreenHeight/gridSize {
return true
}
// is newhead collision
for _, snakeBody := range snake {
if snakeBody == newHead {
return true
}
}
return false
}
// updateSnake - update snake with direction
func (g *Game) updateSnake(snake *[]Point, direction Point) {
head := (*snake)[0]
newHead := Point{
x: head.x + direction.x,
y: head.y + direction.y,
}
// check collision for snake
if g.isBadCollision(newHead, *snake) {
g.gameOver = true
return
}
// check collision with food
if newHead == g.food {
*snake = append(
[]Point{newHead},
*snake...,
)
g.SpawnFood()
} else {
*snake = append(
[]Point{newHead},
(*snake)[:len(*snake)-1]...,
)
}
}
// drawGameOverText - draw game over text on screen
func (g *Game) drawGameOverText(screen *ebiten.Image) {
face := &text.GoTextFace{
Source: MplusFaceSource,
Size: 48,
}
title := "Game Over!"
w, h := text.Measure(title,
face,
face.Size,
)
op := &text.DrawOptions{}
op.GeoM.Translate(ScreenWidth/2-w/2, ScreenHeight/2-h/2)
op.ColorScale.ScaleWithColor(color.White)
text.Draw(
screen,
title,
face,
op,
)
}
// Draw - handle screen update
func (g *Game) Draw(screen *ebiten.Image) {
for _, p := range g.snake {
vector.DrawFilledRect(
screen,
float32(p.x*gridSize),
float32(p.y*gridSize),
gridSize,
gridSize,
color.White,
true,
)
}
vector.DrawFilledRect(
screen,
float32(g.food.x*gridSize),
float32(g.food.y*gridSize),
gridSize,
gridSize,
color.RGBA{255, 0, 0, 255},
true,
)
if g.gameOver {
g.drawGameOverText(screen)
}
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return ScreenWidth, ScreenHeight
}
// SpawnFood - generate new food
func (g *Game) SpawnFood() {
g.food = Point{
x: g.randGenerator.Intn(ScreenWidth / gridSize),
y: g.randGenerator.Intn(ScreenHeight / gridSize),
}
}
// NewGame - create Game
func NewGame() *Game {
return &Game{
snake: []Point{{
x: ScreenWidth / gridSize / 2,
y: ScreenHeight / gridSize / 2,
}},
direction: Point{x: 1, y: 0},
randGenerator: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}