@@ -194,4 +194,389 @@ func NewGame() *Game {
194
194
195
195
## game screen
196
196
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