Skip to content

Commit 813ab89

Browse files
committed
add ebiten example (a tiny snake game, powered by ebiten)
Signed-off-by: Simon Waldherr <[email protected]>
1 parent bd2b42b commit 813ab89

File tree

1 file changed

+215
-0
lines changed

1 file changed

+215
-0
lines changed

non-std-lib/ebiten.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"image/color"
6+
"log"
7+
"math/rand"
8+
"time"
9+
10+
"github.com/hajimehoshi/ebiten/v2"
11+
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
12+
)
13+
14+
const (
15+
screenWidth = 320 // Width of the playing field
16+
screenHeight = 240 // Height of the playing field
17+
gridSize = 10 // Size of each segment of the snake and the food
18+
initialSpeed = 8 // Initial speed of the snake
19+
margin = 2 // Margin to prevent food from appearing at the edge
20+
)
21+
22+
type Game struct {
23+
snake []Position // Position of the snake
24+
direction Direction // Current movement direction
25+
food Position // Position of the food
26+
score int // Player's score
27+
gameOver bool // Game status
28+
speed int // Game speed
29+
frameCount int // Counts frames to control the speed
30+
}
31+
32+
type Position struct {
33+
X int
34+
Y int
35+
}
36+
37+
type Direction struct {
38+
X int
39+
Y int
40+
}
41+
42+
// Layout defines the size of the game window
43+
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
44+
return screenWidth, screenHeight
45+
}
46+
47+
// Update contains the game logic that is updated every frame
48+
func (g *Game) Update() error {
49+
// If "Escape" is pressed, the game ends
50+
if ebiten.IsKeyPressed(ebiten.KeyEscape) {
51+
return fmt.Errorf("Game ended")
52+
}
53+
54+
// If the game is over, it can be restarted by pressing the spacebar
55+
if g.gameOver {
56+
if ebiten.IsKeyPressed(ebiten.KeySpace) {
57+
*g = *NewGame() // Reset the game
58+
}
59+
return nil
60+
}
61+
62+
// Process input (change direction)
63+
g.handleInput()
64+
65+
// Count frames and control snake movement
66+
g.frameCount++
67+
if g.frameCount%g.speed != 0 {
68+
return nil
69+
}
70+
71+
// Move the snake
72+
g.moveSnake()
73+
74+
// Check for collisions (with the wall or itself)
75+
g.checkCollisions()
76+
77+
return nil
78+
}
79+
80+
// handleInput processes the player's keyboard input
81+
func (g *Game) handleInput() {
82+
// Change direction, but prevent the snake from reversing directly
83+
if ebiten.IsKeyPressed(ebiten.KeyArrowUp) && g.direction.Y == 0 {
84+
g.direction = Direction{X: 0, Y: -1}
85+
}
86+
if ebiten.IsKeyPressed(ebiten.KeyArrowDown) && g.direction.Y == 0 {
87+
g.direction = Direction{X: 0, Y: 1}
88+
}
89+
if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) && g.direction.X == 0 {
90+
g.direction = Direction{X: -1, Y: 0}
91+
}
92+
if ebiten.IsKeyPressed(ebiten.KeyArrowRight) && g.direction.X == 0 {
93+
g.direction = Direction{X: 1, Y: 0}
94+
}
95+
}
96+
97+
// moveSnake moves the snake and checks if it has reached the food
98+
func (g *Game) moveSnake() {
99+
// New head position based on the current direction
100+
newHead := Position{
101+
X: g.snake[0].X + g.direction.X*gridSize,
102+
Y: g.snake[0].Y + g.direction.Y*gridSize,
103+
}
104+
105+
// Add the new head to the snake
106+
g.snake = append([]Position{newHead}, g.snake...)
107+
108+
// Check if the snake has reached the food
109+
if newHead == g.food {
110+
g.score++ // Increase score
111+
g.spawnFood()
112+
113+
// Increase speed every 5 points until a limit is reached
114+
if g.score%5 == 0 && g.speed > 2 {
115+
g.speed--
116+
}
117+
} else {
118+
// Remove the last tail segment to move the snake
119+
g.snake = g.snake[:len(g.snake)-1]
120+
}
121+
}
122+
123+
// checkCollisions checks if the snake collides with the wall or itself
124+
func (g *Game) checkCollisions() {
125+
head := g.snake[0]
126+
127+
// Collision with the wall
128+
if head.X < 0 || head.Y < 0 || head.X >= screenWidth || head.Y >= screenHeight {
129+
g.gameOver = true
130+
}
131+
132+
// Collision with itself
133+
for _, segment := range g.snake[1:] {
134+
if head == segment {
135+
g.gameOver = true
136+
break
137+
}
138+
}
139+
}
140+
141+
// Draw draws the snake, food, and score on the screen
142+
func (g *Game) Draw(screen *ebiten.Image) {
143+
// Set background to black
144+
screen.Fill(color.Black)
145+
146+
// Draw the snake (white color)
147+
for _, segment := range g.snake {
148+
ebitenutil.DrawRect(screen, float64(segment.X), float64(segment.Y), gridSize, gridSize, color.White)
149+
}
150+
151+
// Draw the food (red color)
152+
ebitenutil.DrawRect(screen, float64(g.food.X), float64(g.food.Y), gridSize, gridSize, color.RGBA{255, 0, 0, 255})
153+
154+
// Display score and game-over message
155+
if g.gameOver {
156+
msg := fmt.Sprintf("Game Over! Score: %d\nPress 'Space' to restart.", g.score)
157+
ebitenutil.DebugPrintAt(screen, msg, screenWidth/2-80, screenHeight/2)
158+
} else {
159+
msg := fmt.Sprintf("Score: %d", g.score)
160+
ebitenutil.DebugPrint(screen, msg)
161+
}
162+
}
163+
164+
// spawnFood generates a new food object at a random valid position
165+
func (g *Game) spawnFood() {
166+
rand.Seed(time.Now().UnixNano())
167+
168+
// Random position within the playing field, but not directly at the edge (with margin)
169+
for {
170+
g.food = Position{
171+
X: rand.Intn(screenWidth/gridSize-2*margin)*gridSize + margin*gridSize,
172+
Y: rand.Intn(screenHeight/gridSize-2*margin)*gridSize + margin*gridSize,
173+
}
174+
175+
// Ensure the food doesn't appear on the snake
176+
collision := false
177+
for _, segment := range g.snake {
178+
if g.food == segment {
179+
collision = true
180+
break
181+
}
182+
}
183+
// If there's no collision, the position is valid
184+
if !collision {
185+
break
186+
}
187+
}
188+
}
189+
190+
// NewGame initializes a new game
191+
func NewGame() *Game {
192+
return &Game{
193+
snake: []Position{{
194+
X: screenWidth / 2,
195+
Y: screenHeight / 2},
196+
}, // Start the snake in the middle
197+
direction: Direction{X: 0, Y: -1}, // Start moving upwards
198+
speed: initialSpeed, // Initial speed
199+
food: Position{
200+
X: rand.Intn(screenWidth/gridSize-2*margin)*gridSize + margin*gridSize,
201+
Y: rand.Intn(screenHeight/gridSize-2*margin)*gridSize + margin*gridSize,
202+
},
203+
}
204+
}
205+
206+
// main starts the game
207+
func main() {
208+
ebiten.SetWindowSize(screenWidth*2, screenHeight*2) // Window size
209+
ebiten.SetWindowTitle("Snake Game") // Window title
210+
211+
// Run the game
212+
if err := ebiten.RunGame(NewGame()); err != nil {
213+
log.Fatal(err)
214+
}
215+
}

0 commit comments

Comments
 (0)