Created
July 30, 2020 14:04
-
-
Save shazow/c92d0d6245a4c6b9eca417aaa1c2691d to your computer and use it in GitHub Desktop.
Revisions
-
shazow created this gist
Jul 30, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,253 @@ package main import ( "context" "fmt" "os" "time" "github.com/gdamore/tcell" ) func exit(code int, format string, args ...interface{}) { fmt.Fprintf(os.Stderr, format, args...) os.Exit(code) } func main() { tcell.SetEncodingFallback(tcell.EncodingFallbackASCII) s, err := tcell.NewScreen() if err != nil { exit(1, "failed to create a new screen") } if err = s.Init(); err != nil { exit(1, "failed to initialize screen") } defer s.Fini() s.SetStyle(tcell.StyleDefault. Foreground(tcell.ColorWhite). Background(tcell.ColorBlack)) s.Clear() g := game{ screen: s, maxFPS: 15, height: 30, width: 40, } g.Reset() if err := g.Run(context.Background()); err != nil { exit(2, "run aborted: %s", err) } } type game struct { screen tcell.Screen maxFPS int width int height int entities []Rendered player *player } // Reset game state, initializing all the starting entities func (g *game) Reset() { g.player = &player{X: g.width / 2, Y: g.height} g.entities = []Rendered{ g.player, Invader(5, 3), Invader(10, 3), Invader(15, 3), } } // Run the game, abort it when context is cancelled func (g *game) Run(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() keyCh := make(chan tcell.Key, 1) go g.listenKeys(ctx, keyCh) var lastTime, currentTime time.Time for { select { case <-ctx.Done(): return nil case key := <-keyCh: if key == tcell.KeyEscape { return nil } if err := g.handle(key); err != nil { return err } default: } currentTime = time.Now() delta := currentTime.Sub(lastTime) fps := int(time.Second / delta) if g.maxFPS > 0 && g.maxFPS < fps { // Throttle game loop to match maxFPS <-time.After(delta - time.Second/time.Duration(g.maxFPS)) continue } if err := g.tick(currentTime.Sub(lastTime)); err != nil { return err } lastTime = currentTime } } // handle is called to react to a key press, it is run in the same goroutine as // the main game loop to avoid race conditions. func (g *game) handle(k tcell.Key) error { switch k { case tcell.KeyLeft: g.player.X -= 1 if g.player.X < 0 { g.player.X = 0 } case tcell.KeyRight: g.player.X += 1 if g.player.X >= g.width { g.player.X = g.width - 1 } case tcell.KeyEnter: // Spawn bullet g.entities = append(g.entities, &bullet{ X: g.player.X, Y: g.player.Y - 1, }) default: // Unhandled, skip clear return nil } g.screen.Clear() return nil } // listenKeys polls for key presses and pipes them into a channel. func (g *game) listenKeys(ctx context.Context, keyCh chan tcell.Key) { for { select { case <-ctx.Done(): return default: } // FIXME: Is it possible to inline this in the game loop rather than do // channel message-passing? The PollEvent docs seem to imply not? ev := g.screen.PollEvent() switch ev := ev.(type) { case *tcell.EventKey: // FIXME: Purge channel if it's full, if we don't care about outdated keys keyCh <- ev.Key() case *tcell.EventResize: g.screen.Sync() } } } // tick performs the tick game loop step func (g *game) tick(delta time.Duration) error { g.screen.Clear() var remove []Rendered for _, entity := range g.entities { if ticker, ok := entity.(Ticked); ok { if !ticker.Tick(delta) { remove = append(remove, entity) continue // Skip rendering } } if err := entity.Render(g.screen); err != nil { return err } } if len(remove) > 0 { // TODO: Remove from rendered } g.screen.Show() return nil } // Entities: type Ticked interface { Tick(delta time.Duration) (keep bool) } type Rendered interface { Render(tcell.Screen) error } func Invader(x, y int) *invader { return &invader{ X: x, Y: y, direction: -1, moveSpeed: 2 * time.Second, turnSpeed: 10 * time.Second, } } type invader struct { X, Y int direction int moveSpeed time.Duration turnSpeed time.Duration timeTurn time.Duration timeMove time.Duration } func (i *invader) Render(s tcell.Screen) error { s.SetCell(i.X, i.Y, tcell.StyleDefault, '👽') return nil } func (i *invader) Tick(delta time.Duration) bool { if i.timeTurn < 0 { i.direction *= -1 if i.direction >= 0 { i.direction = 1 i.Y += 1 } i.timeTurn = i.turnSpeed } if i.timeMove < 0 { i.X += i.direction i.timeMove = i.moveSpeed } i.timeMove -= delta i.timeTurn -= delta return true } type player struct { X, Y int } func (p *player) Render(s tcell.Screen) error { s.SetCell(p.X, p.Y, tcell.StyleDefault, '🚢') return nil } type bullet struct { X, Y int } func (b *bullet) Render(s tcell.Screen) error { s.SetCell(b.X, b.Y, tcell.StyleDefault, '🚀') return nil } func (b *bullet) Tick(delta time.Duration) (keep bool) { b.Y -= 1 return b.Y > 0 }