Skip to content

leetcode-golang-classroom/golang-sample-with-snake-game

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

golang-sample-with-snake-game

This repository is learn how to use ebiten to build a snake game

game logic

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())),
	}
}

game screen

snake-game

refactor original update logic with ECS architecture

move setup to common

const (
	GameSpeed    = time.Second / 6
	ScreenWidth  = 640
	ScreenHeight = 480
	GridSize     = 20
)

define entity interface logic

package entity

import "github.com/hajimehoshi/ebiten/v2"

type Entity interface {
	Update(world worldView) bool
	Draw(screen *ebiten.Image)
	Tag() string
}
var _ Entity = (*Food)(nil)

type Food struct {
	position      math.Point
	randGenerator *rand.Rand
}

func NewFood(randGenerator *rand.Rand) *Food {
	return &Food{
		position:      math.RandomPosition(randGenerator),
		randGenerator: randGenerator,
	}
}

func (f *Food) Update(world worldView) bool {
	return false
}

func (f *Food) Draw(screen *ebiten.Image) {
	vector.DrawFilledRect(
		screen,
		float32(f.position.X*common.GridSize),
		float32(f.position.Y*common.GridSize),
		float32(common.GridSize),
		float32(common.GridSize),
		color.RGBA{255, 0, 0, 255},
		true,
	)
}

func (f *Food) Tag() string {
	return "food"
}

func (f *Food) Respawn() {
	f.position = math.RandomPosition(f.randGenerator)
}
package entity

import (
	"image/color"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/vector"
	"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/common"
	"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/math"
)

var _ Entity = (*Player)(nil)

type Player struct {
	body      []math.Point
	direction math.Point
}

func NewPlayer(start, dir math.Point) *Player {
	return &Player{
		body:      []math.Point{start},
		direction: dir,
	}
}

func (p *Player) Update(worldView worldView) bool {
	newHead := p.body[0].Add(p.direction)

	// check collision for snake
	if newHead.IsBadCollision(p.body) {
		return true
	}

	grow := false
	for _, entity := range worldView.GetEntities("food") {
		food := entity.(*Food)
		// check collision
		if newHead.Equals(food.position) {
			grow = true
			food.Respawn()
			break
		}
	}
	if grow {
		p.body = append(
			[]math.Point{newHead},
			p.body...,
		)
	} else {
		p.body = append(
			[]math.Point{newHead},
			p.body[:len(p.body)-1]...,
		)
	}
	return false
}

func (p *Player) Draw(screen *ebiten.Image) {
	for _, pt := range p.body {
		vector.DrawFilledRect(
			screen,
			float32(pt.X*common.GridSize),
			float32(pt.Y*common.GridSize),
			float32(common.GridSize),
			float32(common.GridSize),
			color.White,
			true,
		)
	}
}

func (p *Player) SetDirection(dir math.Point) {
	p.direction = dir
}

func (p Player) Tag() string {
	return "player"
}

define worldview interface for avoid circular dependency

package entity

type worldView interface {
	GetEntities(tag string) []Entity
}

define game package for handle worldview

package game

import "github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/entity"

type World struct {
	entities []entity.Entity
}

func NewWorld() *World {
	return &World{
		entities: []entity.Entity{},
	}
}

func (w *World) AddEntity(entity entity.Entity) {
	w.entities = append(w.entities, entity)
}

func (w *World) Entities() []entity.Entity {
	return w.entities
}

func (w World) GetEntities(tag string) []entity.Entity {
	var result []entity.Entity
	for _, e := range w.entities {
		if e.Tag() == tag {
			result = append(result, e)
		}
	}

	return result
}

setup collision logic in math package

package math

import (
	"math/rand"

	"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/common"
)

type Point struct {
	X, Y int
}

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}
)

func (p Point) Equals(other Point) bool {
	return p.X == other.X && p.Y == other.Y
}

func (p Point) Add(other Point) Point {
	return Point{
		X: p.X + other.X,
		Y: p.Y + other.Y,
	}
}

// IsBadCollision - check if snake is collision
func (p Point) IsBadCollision(
	points []Point,
) bool {
	// check if out of bound
	if p.X < 0 || p.Y < 0 ||
		p.X >= common.ScreenWidth/common.GridSize || p.Y >= common.ScreenHeight/common.GridSize {
		return true
	}
	// is newhead collision
	for _, snakeBody := range points {
		if snakeBody == p {
			return true
		}
	}
	return false
}

// RandomPosition
func RandomPosition(randGenerator *rand.Rand) Point {
	return Point{
		X: randGenerator.Intn(common.ScreenWidth / common.GridSize),
		Y: randGenerator.Intn(common.ScreenHeight / common.GridSize),
	}
}

setup game object render in game.go

package internal

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

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/text/v2"
	"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/common"
	"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/entity"
	"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/game"
	"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/math"
)

var (
	MplusFaceSource *text.GoTextFaceSource
)

type Game struct {
	world      *game.World
	lastUpdate time.Time
	gameOver   bool
}

func (g *Game) Update() error {
	if g.gameOver {
		return nil
	}
	playerRaw, ok := g.world.GetFirstEntity("player")
	if !ok {
		return errors.New("entity player was not found")
	}
	player := playerRaw.(*entity.Player)

	// handle key
	if ebiten.IsKeyPressed(ebiten.KeyW) {
		player.SetDirection(math.DirUp)
	} else if ebiten.IsKeyPressed(ebiten.KeyS) {
		player.SetDirection(math.DirDown)
	} else if ebiten.IsKeyPressed(ebiten.KeyA) {
		player.SetDirection(math.DirLeft)
	} else if ebiten.IsKeyPressed(ebiten.KeyD) {
		player.SetDirection(math.DirRight)
	}
	// slow down
	if time.Since(g.lastUpdate) < common.GameSpeed {
		return nil
	}
	g.lastUpdate = time.Now()

	for _, entity := range g.world.Entities() {
		if entity.Update(g.world) {
			g.gameOver = true
			return nil
		}
	}
	return nil
}

// 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(common.ScreenWidth/2-w/2, common.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 _, entity := range g.world.Entities() {
		entity.Draw(screen)
	}
	if g.gameOver {
		g.drawGameOverText(screen)
	}
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
	return common.ScreenWidth, common.ScreenHeight
}

// NewGame - create Game
func NewGame() *Game {
	world := game.NewWorld()
	world.AddEntity(
		entity.NewPlayer(
			math.Point{
				X: common.ScreenWidth / common.GridSize / 2,
				Y: common.ScreenHeight / common.GridSize / 2,
			},
			math.DirRight,
		),
	)
	randomGenerator := rand.New(rand.NewSource(time.Now().UnixNano()))
	for i := 0; i < 10; i++ {
		world.AddEntity(
			entity.NewFood(
				randomGenerator,
			),
		)
	}
	return &Game{
		world: world,
	}
}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages