diff --git a/internal/game/entity/command/commands.go b/internal/game/entity/command/commands.go index ae3c950..e984882 100644 --- a/internal/game/entity/command/commands.go +++ b/internal/game/entity/command/commands.go @@ -9,13 +9,13 @@ const ( SET_ANIMATION ) -type EntityCommand struct { +type Command struct { key int value float64 } -func NewEntityCommand(key int, value float64) EntityCommand { - return EntityCommand{ +func NewCommand(key int, value float64) Command { + return Command{ key: key, value: value, } diff --git a/internal/game/entity/command/handler.go b/internal/game/entity/command/handler.go index ed84bdf..843bf41 100644 --- a/internal/game/entity/command/handler.go +++ b/internal/game/entity/command/handler.go @@ -21,7 +21,7 @@ type CommandHandler struct { ctx context.Context timeout time.Duration entity Entity - commandChan chan EntityCommand + commandChan chan Command drawRequestChan chan bool drawResponseChan chan bool updateRequestChan chan bool @@ -29,7 +29,7 @@ type CommandHandler struct { } func NewCommandHandler(ctx context.Context, entity Entity) *CommandHandler { - commandChan := make(chan EntityCommand, 10) + commandChan := make(chan Command, 10) drawRequestChan := make(chan bool) drawResponseChan := make(chan bool) updateRequestChan := make(chan bool) @@ -59,7 +59,7 @@ func (c *CommandHandler) CloseRequests() { close(c.commandChan) } -func (c *CommandHandler) CommandRequest() chan EntityCommand { +func (c *CommandHandler) CommandRequest() chan Command { return c.commandChan } @@ -103,7 +103,7 @@ func (c *CommandHandler) Run() { }() case cmd := <-c.commandChan: wg.Add(1) - go func(cmd EntityCommand) { + go func(cmd Command) { defer wg.Done() c.HandleWithTimeout(cmd) }(cmd) @@ -127,7 +127,7 @@ func (c *CommandHandler) Run() { } } -func (c *CommandHandler) HandleWithTimeout(cmd EntityCommand) { +func (c *CommandHandler) HandleWithTimeout(cmd Command) { err := channels.RunWithTimeout(c.timeout, func() error { return c.Handle(cmd) }) @@ -136,7 +136,7 @@ func (c *CommandHandler) HandleWithTimeout(cmd EntityCommand) { } } -func (c *CommandHandler) Handle(cmd EntityCommand) error { +func (c *CommandHandler) Handle(cmd Command) error { switch cmd.key { case MOVE_X: c.entity.MoveX(cmd.value) diff --git a/internal/game/game.go b/internal/game/game.go index 8165e5a..f76bf85 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -13,6 +13,7 @@ import ( "gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity/types" "gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/player" "gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/sprite" + "gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/window" "gitea.wisellama.rocks/Project-Ely/project-ely/internal/vector" "gitea.wisellama.rocks/Wisellama/gosimpleconf" "github.com/veandco/go-sdl2/sdl" @@ -21,7 +22,7 @@ import ( type EntityCmdHandler interface { Run() CloseRequests() - CommandRequest() chan command.EntityCommand + CommandRequest() chan command.Command DrawRequest() chan bool DrawResponse() chan bool UpdateRequest() chan bool @@ -42,13 +43,13 @@ func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error { } framerateDelay := uint32(1000 / framerate) - err = SdlInit() + err = window.SdlInit() if err != nil { return err } - defer SdlQuit() + defer window.SdlQuit() - gameWindow, err := NewWindow(configMap["game.title"]) + gameWindow, err := window.NewWindow(configMap["game.title"]) if err != nil { err = fmt.Errorf("failed creating GameWindow: %w", err) return err @@ -81,6 +82,8 @@ func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error { animation.DefineAnimations() + inputHandler := window.NewInputHandler(ctx) + // Done with main setup, now moving on to creating specific entities entityList := make([]EntityCmdHandler, 0) @@ -88,7 +91,7 @@ func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error { // Setup Player 1 // Let them control a penguin to start with - player1 := player.NewPlayer(ctx) + player1 := player.NewPlayer(ctx, inputHandler) penguinEntity := types.NewPenguin(renderer) penguinCmdHandler := command.NewCommandHandler(ctx, penguinEntity) penguinEntity.SetSpeed(2.0) @@ -115,13 +118,14 @@ func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error { }() } - // And now starting the main loop + // Start the inputHandler wg.Add(1) go func() { defer wg.Done() - player1.Run() + inputHandler.Run() }() + // And now starting the main loop running := true for running { // Poll for SDL events @@ -139,8 +143,8 @@ func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error { log.Println("Esc quitting") cancel() } else { - // Publish the event so other components can do whatever they need with it - player1.GetSdlEventsChan() <- *e + // Publish the event to the inputHandler + inputHandler.KeyboardChan() <- *e } } @@ -159,6 +163,9 @@ func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error { break } + // Players + player1.Update() + // Background sdl.Do(func() { err = renderer.SetDrawColor(0, 120, 0, 255) @@ -191,7 +198,7 @@ func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error { }) } - player1.Cleanup() + inputHandler.CloseChannels() for _, e := range entityList { e.CloseRequests() diff --git a/internal/game/player/player.go b/internal/game/player/player.go index c633ab1..cfbee99 100644 --- a/internal/game/player/player.go +++ b/internal/game/player/player.go @@ -2,138 +2,67 @@ package player import ( "context" - "log" - "sync" "time" - "gitea.wisellama.rocks/Project-Ely/project-ely/internal/channels" "gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity/command" "github.com/veandco/go-sdl2/sdl" ) -// player represents a collection of stuff controlled by the user's input. +type InputHandler interface { + Keystate(keycode sdl.Keycode) bool +} + +// A player represents a collection of stuff controlled by the user's input. // It contains the camera used to view the world, // the viewport for the part of the screen to draw to (splitscreen support), // as well as the entity the player is currently controlling. type player struct { - mx sync.RWMutex - ctx context.Context - timeout time.Duration - sdlKeyboardEventsChan chan sdl.KeyboardEvent + ctx context.Context + timeout time.Duration + inputHandler InputHandler - entityChan chan command.EntityCommand - keystates map[sdl.Keycode]bool + entityChan chan command.Command } -func NewPlayer(ctx context.Context) *player { - sdlKeyboardEventsChan := make(chan sdl.KeyboardEvent, 10) +func NewPlayer(ctx context.Context, inputHandler InputHandler) *player { defaultTimeout := time.Second - keystates := make(map[sdl.Keycode]bool) p := player{ - ctx: ctx, - timeout: defaultTimeout, - sdlKeyboardEventsChan: sdlKeyboardEventsChan, - keystates: keystates, + ctx: ctx, + timeout: defaultTimeout, + inputHandler: inputHandler, } return &p } -func (p *player) Cleanup() { - p.mx.Lock() - defer p.mx.Unlock() - - close(p.sdlKeyboardEventsChan) -} - -func (p *player) SetEntityChan(e chan command.EntityCommand) { - p.mx.Lock() - defer p.mx.Unlock() - +func (p *player) SetEntityChan(e chan command.Command) { p.entityChan = e } -func (p *player) GetSdlEventsChan() chan sdl.KeyboardEvent { - p.mx.RLock() - defer p.mx.RUnlock() - - return p.sdlKeyboardEventsChan -} - -func (p *player) Run() { - running := true - for running { - select { - case e := <-p.sdlKeyboardEventsChan: - go func(event sdl.KeyboardEvent) { - p.HandleWithTimeout(event) - }(e) - case <-p.ctx.Done(): - // Graceful shutdown - running = false - // Finish up anything in the queue - for e := range p.sdlKeyboardEventsChan { - p.HandleWithTimeout(e) - } - } - } -} - -func (p *player) HandleWithTimeout(event sdl.KeyboardEvent) { - err := channels.RunWithTimeout(p.timeout, func() error { - return p.Handle(event) - }) - if err != nil { - log.Printf("%v\n", err) - } -} - -func (p *player) Handle(e sdl.KeyboardEvent) error { - p.mx.Lock() - defer p.mx.Unlock() - - // Key states (just set a boolean whether the key is actively being pressed) - keystateChanged := false - if e.Type == sdl.KEYDOWN { - if !p.keystates[e.Keysym.Sym] { - keystateChanged = true - } - p.keystates[e.Keysym.Sym] = true - } else if e.Type == sdl.KEYUP { - if p.keystates[e.Keysym.Sym] { - keystateChanged = true - } - p.keystates[e.Keysym.Sym] = false - } - - // Only send events to the entity if something actually changed - if !keystateChanged { - return nil - } - +func (p *player) Update() error { // Speed - if p.keystates[sdl.K_LSHIFT] { - p.entityChan <- command.NewEntityCommand(command.SET_SPEED, 4) + if p.inputHandler.Keystate(sdl.K_LSHIFT) { + p.entityChan <- command.NewCommand(command.SET_SPEED, 4) } else { - p.entityChan <- command.NewEntityCommand(command.SET_SPEED, 2) + p.entityChan <- command.NewCommand(command.SET_SPEED, 2) } // Move X - if p.keystates[sdl.K_d] { - p.entityChan <- command.NewEntityCommand(command.MOVE_X, 1.0) - } else if p.keystates[sdl.K_a] { - p.entityChan <- command.NewEntityCommand(command.MOVE_X, -1.0) - } else if !p.keystates[sdl.K_d] && !p.keystates[sdl.K_a] { - p.entityChan <- command.NewEntityCommand(command.MOVE_X, 0.0) + if p.inputHandler.Keystate(sdl.K_d) { + p.entityChan <- command.NewCommand(command.MOVE_X, 1.0) + } else if p.inputHandler.Keystate(sdl.K_a) { + p.entityChan <- command.NewCommand(command.MOVE_X, -1.0) + } else if !p.inputHandler.Keystate(sdl.K_d) && !p.inputHandler.Keystate(sdl.K_a) { + p.entityChan <- command.NewCommand(command.MOVE_X, 0.0) } // Move Y - if p.keystates[sdl.K_w] { - p.entityChan <- command.NewEntityCommand(command.MOVE_Y, -1.0) - } else if p.keystates[sdl.K_s] { - p.entityChan <- command.NewEntityCommand(command.MOVE_Y, 1.0) - } else if !p.keystates[sdl.K_w] && !p.keystates[sdl.K_s] { - p.entityChan <- command.NewEntityCommand(command.MOVE_Y, 0.0) + if p.inputHandler.Keystate(sdl.K_w) { + p.entityChan <- command.NewCommand(command.MOVE_Y, -1.0) + } else if p.inputHandler.Keystate(sdl.K_s) { + p.entityChan <- command.NewCommand(command.MOVE_Y, 1.0) + } else if !p.inputHandler.Keystate(sdl.K_w) && !p.inputHandler.Keystate(sdl.K_s) { + p.entityChan <- command.NewCommand(command.MOVE_Y, 0.0) } return nil } diff --git a/internal/game/window/input.go b/internal/game/window/input.go new file mode 100644 index 0000000..42c8020 --- /dev/null +++ b/internal/game/window/input.go @@ -0,0 +1,75 @@ +package window + +import ( + "context" + "sync" + + "github.com/veandco/go-sdl2/sdl" +) + +type inputHandler struct { + ctx context.Context + + keyboardChan chan sdl.KeyboardEvent + + // The actual internal state + mxKeyboard sync.RWMutex + keystates map[sdl.Keycode]bool + // TODO joystick/gamepad + // mxJoystick sync.RWMutex +} + +func NewInputHandler(ctx context.Context) *inputHandler { + keyboardChan := make(chan sdl.KeyboardEvent) + keystates := make(map[sdl.Keycode]bool) + + i := inputHandler{ + ctx: ctx, + keystates: keystates, + keyboardChan: keyboardChan, + } + return &i +} + +func (i *inputHandler) CloseChannels() { + close(i.keyboardChan) +} + +func (i *inputHandler) KeyboardChan() chan sdl.KeyboardEvent { + return i.keyboardChan +} + +func (i *inputHandler) Run() { + running := true + for running { + select { + case event := <-i.keyboardChan: + i.UpdateKeyboard(event) + case <-i.ctx.Done(): + running = false + } + } + + // Finish up anything in the queues + for e := range i.keyboardChan { + i.UpdateKeyboard(e) + } +} + +func (i *inputHandler) UpdateKeyboard(e sdl.KeyboardEvent) { + i.mxKeyboard.Lock() + defer i.mxKeyboard.Unlock() + + // Key states (just set a boolean whether the key is actively being pressed) + if e.Type == sdl.KEYDOWN { + i.keystates[e.Keysym.Sym] = true + } else if e.Type == sdl.KEYUP { + i.keystates[e.Keysym.Sym] = false + } +} + +func (i *inputHandler) Keystate(keycode sdl.Keycode) bool { + i.mxKeyboard.RLock() + defer i.mxKeyboard.RUnlock() + return i.keystates[keycode] +} diff --git a/internal/game/sdl.go b/internal/game/window/sdl.go similarity index 99% rename from internal/game/sdl.go rename to internal/game/window/sdl.go index 0d5580a..1d087d4 100644 --- a/internal/game/sdl.go +++ b/internal/game/window/sdl.go @@ -1,4 +1,4 @@ -package game +package window import ( "fmt" diff --git a/internal/game/window.go b/internal/game/window/window.go similarity index 98% rename from internal/game/window.go rename to internal/game/window/window.go index a958137..6c113a4 100644 --- a/internal/game/window.go +++ b/internal/game/window/window.go @@ -1,4 +1,4 @@ -package game +package window import ( "log" diff --git a/main.go b/main.go index 653713e..2afa421 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,9 @@ func run(ctx context.Context) error { } defer logWriter.Cleanup() + // Setup Random Number Generator seed + config.InitRNG() + // Start everything with the SDL goroutine context log.Printf("=== Starting %v ===", configMap["game.title"]) sdl.Main(func() {