@@ -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