Skip to content

Commit 1352e55

Browse files
committed
✨ (refactor): add entity logic
1 parent 8305003 commit 1352e55

File tree

10 files changed

+682
-114
lines changed

10 files changed

+682
-114
lines changed

README.md

Lines changed: 386 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,389 @@ func NewGame() *Game {
194194

195195
## game screen
196196

197-
![snake-game](snake-game.png)
197+
![snake-game](snake-game.png)
198+
199+
## refactor original update logic with ECS architecture
200+
201+
### move setup to common
202+
203+
```golang
204+
const (
205+
GameSpeed = time.Second / 6
206+
ScreenWidth = 640
207+
ScreenHeight = 480
208+
GridSize = 20
209+
)
210+
```
211+
212+
### define entity interface logic
213+
214+
```golang
215+
package entity
216+
217+
import "github.com/hajimehoshi/ebiten/v2"
218+
219+
type Entity interface {
220+
Update(world worldView) bool
221+
Draw(screen *ebiten.Image)
222+
Tag() string
223+
}
224+
```
225+
226+
```golang
227+
var _ Entity = (*Food)(nil)
228+
229+
type Food struct {
230+
position math.Point
231+
randGenerator *rand.Rand
232+
}
233+
234+
func NewFood(randGenerator *rand.Rand) *Food {
235+
return &Food{
236+
position: math.RandomPosition(randGenerator),
237+
randGenerator: randGenerator,
238+
}
239+
}
240+
241+
func (f *Food) Update(world worldView) bool {
242+
return false
243+
}
244+
245+
func (f *Food) Draw(screen *ebiten.Image) {
246+
vector.DrawFilledRect(
247+
screen,
248+
float32(f.position.X*common.GridSize),
249+
float32(f.position.Y*common.GridSize),
250+
float32(common.GridSize),
251+
float32(common.GridSize),
252+
color.RGBA{255, 0, 0, 255},
253+
true,
254+
)
255+
}
256+
257+
func (f *Food) Tag() string {
258+
return "food"
259+
}
260+
261+
func (f *Food) Respawn() {
262+
f.position = math.RandomPosition(f.randGenerator)
263+
}
264+
265+
```
266+
267+
```golang
268+
package entity
269+
270+
import (
271+
"image/color"
272+
273+
"github.com/hajimehoshi/ebiten/v2"
274+
"github.com/hajimehoshi/ebiten/v2/vector"
275+
"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/common"
276+
"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/math"
277+
)
278+
279+
var _ Entity = (*Player)(nil)
280+
281+
type Player struct {
282+
body []math.Point
283+
direction math.Point
284+
}
285+
286+
func NewPlayer(start, dir math.Point) *Player {
287+
return &Player{
288+
body: []math.Point{start},
289+
direction: dir,
290+
}
291+
}
292+
293+
func (p *Player) Update(worldView worldView) bool {
294+
newHead := p.body[0].Add(p.direction)
295+
296+
// check collision for snake
297+
if newHead.IsBadCollision(p.body) {
298+
return true
299+
}
300+
301+
grow := false
302+
for _, entity := range worldView.GetEntities("food") {
303+
food := entity.(*Food)
304+
// check collision
305+
if newHead.Equals(food.position) {
306+
grow = true
307+
food.Respawn()
308+
break
309+
}
310+
}
311+
if grow {
312+
p.body = append(
313+
[]math.Point{newHead},
314+
p.body...,
315+
)
316+
} else {
317+
p.body = append(
318+
[]math.Point{newHead},
319+
p.body[:len(p.body)-1]...,
320+
)
321+
}
322+
return false
323+
}
324+
325+
func (p *Player) Draw(screen *ebiten.Image) {
326+
for _, pt := range p.body {
327+
vector.DrawFilledRect(
328+
screen,
329+
float32(pt.X*common.GridSize),
330+
float32(pt.Y*common.GridSize),
331+
float32(common.GridSize),
332+
float32(common.GridSize),
333+
color.White,
334+
true,
335+
)
336+
}
337+
}
338+
339+
func (p *Player) SetDirection(dir math.Point) {
340+
p.direction = dir
341+
}
342+
343+
func (p Player) Tag() string {
344+
return "player"
345+
}
346+
347+
```
348+
349+
### define worldview interface for avoid circular dependency
350+
351+
```golang
352+
package entity
353+
354+
type worldView interface {
355+
GetEntities(tag string) []Entity
356+
}
357+
358+
```
359+
360+
### define game package for handle worldview
361+
362+
```golang
363+
package game
364+
365+
import "github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/entity"
366+
367+
type World struct {
368+
entities []entity.Entity
369+
}
370+
371+
func NewWorld() *World {
372+
return &World{
373+
entities: []entity.Entity{},
374+
}
375+
}
376+
377+
func (w *World) AddEntity(entity entity.Entity) {
378+
w.entities = append(w.entities, entity)
379+
}
380+
381+
func (w *World) Entities() []entity.Entity {
382+
return w.entities
383+
}
384+
385+
func (w World) GetEntities(tag string) []entity.Entity {
386+
var result []entity.Entity
387+
for _, e := range w.entities {
388+
if e.Tag() == tag {
389+
result = append(result, e)
390+
}
391+
}
392+
393+
return result
394+
}
395+
```
396+
397+
### setup collision logic in math package
398+
399+
```golang
400+
package math
401+
402+
import (
403+
"math/rand"
404+
405+
"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/common"
406+
)
407+
408+
type Point struct {
409+
X, Y int
410+
}
411+
412+
var (
413+
DirUp = Point{X: 0, Y: -1}
414+
DirDown = Point{X: 0, Y: 1}
415+
DirLeft = Point{X: -1, Y: 0}
416+
DirRight = Point{X: 1, Y: 0}
417+
)
418+
419+
func (p Point) Equals(other Point) bool {
420+
return p.X == other.X && p.Y == other.Y
421+
}
422+
423+
func (p Point) Add(other Point) Point {
424+
return Point{
425+
X: p.X + other.X,
426+
Y: p.Y + other.Y,
427+
}
428+
}
429+
430+
// IsBadCollision - check if snake is collision
431+
func (p Point) IsBadCollision(
432+
points []Point,
433+
) bool {
434+
// check if out of bound
435+
if p.X < 0 || p.Y < 0 ||
436+
p.X >= common.ScreenWidth/common.GridSize || p.Y >= common.ScreenHeight/common.GridSize {
437+
return true
438+
}
439+
// is newhead collision
440+
for _, snakeBody := range points {
441+
if snakeBody == p {
442+
return true
443+
}
444+
}
445+
return false
446+
}
447+
448+
// RandomPosition
449+
func RandomPosition(randGenerator *rand.Rand) Point {
450+
return Point{
451+
X: randGenerator.Intn(common.ScreenWidth / common.GridSize),
452+
Y: randGenerator.Intn(common.ScreenHeight / common.GridSize),
453+
}
454+
}
455+
456+
```
457+
458+
### setup game object render in game.go
459+
460+
```golang
461+
package internal
462+
463+
import (
464+
"errors"
465+
"image/color"
466+
"math/rand"
467+
"time"
468+
469+
"github.com/hajimehoshi/ebiten/v2"
470+
"github.com/hajimehoshi/ebiten/v2/text/v2"
471+
"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/common"
472+
"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/entity"
473+
"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/game"
474+
"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal/math"
475+
)
476+
477+
var (
478+
MplusFaceSource *text.GoTextFaceSource
479+
)
480+
481+
type Game struct {
482+
world *game.World
483+
lastUpdate time.Time
484+
gameOver bool
485+
}
486+
487+
func (g *Game) Update() error {
488+
if g.gameOver {
489+
return nil
490+
}
491+
playerRaw, ok := g.world.GetFirstEntity("player")
492+
if !ok {
493+
return errors.New("entity player was not found")
494+
}
495+
player := playerRaw.(*entity.Player)
496+
497+
// handle key
498+
if ebiten.IsKeyPressed(ebiten.KeyW) {
499+
player.SetDirection(math.DirUp)
500+
} else if ebiten.IsKeyPressed(ebiten.KeyS) {
501+
player.SetDirection(math.DirDown)
502+
} else if ebiten.IsKeyPressed(ebiten.KeyA) {
503+
player.SetDirection(math.DirLeft)
504+
} else if ebiten.IsKeyPressed(ebiten.KeyD) {
505+
player.SetDirection(math.DirRight)
506+
}
507+
// slow down
508+
if time.Since(g.lastUpdate) < common.GameSpeed {
509+
return nil
510+
}
511+
g.lastUpdate = time.Now()
512+
513+
for _, entity := range g.world.Entities() {
514+
if entity.Update(g.world) {
515+
g.gameOver = true
516+
return nil
517+
}
518+
}
519+
return nil
520+
}
521+
522+
// drawGameOverText - draw game over text on screen
523+
func (g *Game) drawGameOverText(screen *ebiten.Image) {
524+
face := &text.GoTextFace{
525+
Source: MplusFaceSource,
526+
Size: 48,
527+
}
528+
title := "Game Over!"
529+
w, h := text.Measure(title,
530+
face,
531+
face.Size,
532+
)
533+
op := &text.DrawOptions{}
534+
op.GeoM.Translate(common.ScreenWidth/2-w/2, common.ScreenHeight/2-h/2)
535+
op.ColorScale.ScaleWithColor(color.White)
536+
text.Draw(
537+
screen,
538+
title,
539+
face,
540+
op,
541+
)
542+
}
543+
544+
// Draw - handle screen update
545+
func (g *Game) Draw(screen *ebiten.Image) {
546+
for _, entity := range g.world.Entities() {
547+
entity.Draw(screen)
548+
}
549+
if g.gameOver {
550+
g.drawGameOverText(screen)
551+
}
552+
}
553+
554+
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
555+
return common.ScreenWidth, common.ScreenHeight
556+
}
557+
558+
// NewGame - create Game
559+
func NewGame() *Game {
560+
world := game.NewWorld()
561+
world.AddEntity(
562+
entity.NewPlayer(
563+
math.Point{
564+
X: common.ScreenWidth / common.GridSize / 2,
565+
Y: common.ScreenHeight / common.GridSize / 2,
566+
},
567+
math.DirRight,
568+
),
569+
)
570+
randomGenerator := rand.New(rand.NewSource(time.Now().UnixNano()))
571+
for i := 0; i < 10; i++ {
572+
world.AddEntity(
573+
entity.NewFood(
574+
randomGenerator,
575+
),
576+
)
577+
}
578+
return &Game{
579+
world: world,
580+
}
581+
}
582+
```

0 commit comments

Comments
 (0)