Actually update and draw things asynchronously too, but wait for responses.

main
Sean Hickey 2022-10-23 00:57:23 -07:00
parent b148699a58
commit b3836dc411
10 changed files with 343 additions and 181 deletions

View File

@ -0,0 +1,27 @@
package channels
import (
"context"
)
// BlockingFunction will wait for the function to be completed
// or exit if the context is done.
func BlockingFunction(ctx context.Context, function func() error) error {
var err error
finished := make(chan bool)
go func() {
err = function()
finished <- true
}()
select {
case <-finished:
// Completed
case <-ctx.Done():
// Context closed (e.g. timed out or ctrl+c)
return ctx.Err()
}
return err
}

View File

@ -5,25 +5,12 @@ import (
"time"
)
// RunWithTimeout will run a function in a goroutine with a new timeout context.
// If the context times out, then we exit the goroutine the return the context error.
func RunWithTimeout(timeout time.Duration, function func() error) error {
var err error
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// Launch a goroutine with a channel
finished := make(chan bool)
go func() {
err = function()
finished <- true
}()
// Then wait for completion or timeout
select {
case <-ctx.Done():
// Timed out
return ctx.Err()
case <-finished:
// Completed
}
err := BlockingFunction(ctx, function)
return err
}

View File

@ -1,4 +1,4 @@
package entity
package animation
import (
"github.com/veandco/go-sdl2/sdl"
@ -44,6 +44,10 @@ func (e *entityAnimation) Draw(frame int32, windowPosition *sdl.Point) error {
return e.spriteAnimation.Draw(frame, windowPosition, e.angle, e.center, e.flip)
}
func (e *entityAnimation) GetSpeed() int32 {
return e.speed
}
func DefineAnimations() {
DefinePenguinAnimations()
}

View File

@ -1,11 +1,11 @@
package entity
package animation
import (
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/sprite"
"github.com/veandco/go-sdl2/sdl"
)
var penguinAnimations map[int]*entityAnimation
var PenguinAnimations map[int]*entityAnimation
const (
PENGUIN_WALK_RIGHT int = iota
@ -28,7 +28,7 @@ func DefinePenguinAnimations() {
)
dimensions = sdl.Point{X: 32, Y: 32}
penguinAnimations = make(map[int]*entityAnimation)
PenguinAnimations = make(map[int]*entityAnimation)
// Walking Right is in the spritesheet.
speed = 5
@ -36,14 +36,14 @@ func DefinePenguinAnimations() {
length = 4
center = nil // center is for rotation, nil will default to w/2 h/2
walkRight := sprite.NewAnimation(filename, dimensions, offset, length)
penguinAnimations[PENGUIN_WALK_RIGHT] = NewEntityAnimation(walkRight, speed, length, 0, center, sdl.FLIP_NONE)
PenguinAnimations[PENGUIN_WALK_RIGHT] = NewEntityAnimation(walkRight, speed, length, 0, center, sdl.FLIP_NONE)
// Walking Left is just that flipped.
penguinAnimations[PENGUIN_WALK_LEFT] = NewEntityAnimation(walkRight, speed, length, 0, center, sdl.FLIP_HORIZONTAL)
PenguinAnimations[PENGUIN_WALK_LEFT] = NewEntityAnimation(walkRight, speed, length, 0, center, sdl.FLIP_HORIZONTAL)
// Stationary is just the first frame.
length = 1
stationaryRight := sprite.NewAnimation(filename, dimensions, offset, length)
penguinAnimations[PENGUIN_STATIONARY_RIGHT] = NewEntityAnimation(stationaryRight, speed, length, 0, center, sdl.FLIP_NONE)
penguinAnimations[PENGUIN_STATIONARY_LEFT] = NewEntityAnimation(stationaryRight, speed, length, 0, center, sdl.FLIP_HORIZONTAL)
PenguinAnimations[PENGUIN_STATIONARY_RIGHT] = NewEntityAnimation(stationaryRight, speed, length, 0, center, sdl.FLIP_NONE)
PenguinAnimations[PENGUIN_STATIONARY_LEFT] = NewEntityAnimation(stationaryRight, speed, length, 0, center, sdl.FLIP_HORIZONTAL)
}

View File

@ -1,12 +1,12 @@
package entity
package command
// List of keys supported by all entities
const (
COMMAND_DRAW int = iota
COMMAND_UPDATE
COMMAND_MOVE_X
COMMAND_MOVE_Y
COMMAND_SET_SPEED
MOVE_X int = iota
MOVE_Y
SET_SPEED
SET_POSITION
SET_ANIMATION
)
type EntityCommand struct {

View File

@ -0,0 +1,170 @@
package command
import (
"context"
"log"
"sync"
"time"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/channels"
)
type Entity interface {
Draw() error
Update() error
MoveX(x float64)
MoveY(y float64)
SetSpeed(s float64)
}
type CommandHandler struct {
ctx context.Context
timeout time.Duration
entity Entity
commandChan chan EntityCommand
drawRequestChan chan bool
drawResponseChan chan bool
updateRequestChan chan bool
updateResponseChan chan bool
}
func NewCommandHandler(ctx context.Context, entity Entity) *CommandHandler {
commandChan := make(chan EntityCommand, 10)
drawRequestChan := make(chan bool)
drawResponseChan := make(chan bool)
updateRequestChan := make(chan bool)
updateResponseChan := make(chan bool)
defaultTimeout := time.Second
return &CommandHandler{
ctx: ctx,
timeout: defaultTimeout,
entity: entity,
commandChan: commandChan,
drawRequestChan: drawRequestChan,
drawResponseChan: drawResponseChan,
updateRequestChan: updateRequestChan,
updateResponseChan: updateResponseChan,
}
}
func (c *CommandHandler) closeResponses() {
close(c.drawResponseChan)
close(c.updateResponseChan)
}
func (c *CommandHandler) CloseRequests() {
close(c.drawRequestChan)
close(c.updateRequestChan)
close(c.commandChan)
}
func (c *CommandHandler) CommandRequest() chan EntityCommand {
return c.commandChan
}
func (c *CommandHandler) DrawRequest() chan bool {
return c.drawRequestChan
}
func (c *CommandHandler) DrawResponse() chan bool {
return c.drawResponseChan
}
func (c *CommandHandler) UpdateRequest() chan bool {
return c.updateRequestChan
}
func (c *CommandHandler) UpdateResponse() chan bool {
return c.updateResponseChan
}
func (c *CommandHandler) Run() {
defer c.closeResponses()
wg := sync.WaitGroup{}
running := true
for running {
select {
case <-c.drawRequestChan:
wg.Add(1)
go func() {
defer wg.Done()
c.DrawWithTimeout()
c.drawResponseChan <- true
}()
case <-c.updateRequestChan:
wg.Add(1)
go func() {
defer wg.Done()
c.UpdateWithTimeout()
c.updateResponseChan <- true
}()
case cmd := <-c.commandChan:
wg.Add(1)
go func(cmd EntityCommand) {
defer wg.Done()
c.HandleWithTimeout(cmd)
}(cmd)
case <-c.ctx.Done():
// Graceful shutdown
running = false
}
}
wg.Wait()
// Finish up anything in the queues
for cmd := range c.commandChan {
c.HandleWithTimeout(cmd)
}
for range c.drawRequestChan {
c.UpdateWithTimeout()
}
for range c.drawRequestChan {
c.DrawWithTimeout()
}
}
func (c *CommandHandler) HandleWithTimeout(cmd EntityCommand) {
err := channels.RunWithTimeout(c.timeout, func() error {
return c.Handle(cmd)
})
if err != nil {
log.Printf("handle with timeout error: %v\n", err)
}
}
func (c *CommandHandler) Handle(cmd EntityCommand) error {
switch cmd.key {
case MOVE_X:
c.entity.MoveX(cmd.value)
case MOVE_Y:
c.entity.MoveY(cmd.value)
case SET_SPEED:
c.entity.SetSpeed(cmd.value)
default:
log.Printf("unknown entity command: %v", cmd.key)
}
return nil
}
func (c *CommandHandler) DrawWithTimeout() {
err := channels.RunWithTimeout(c.timeout, func() error {
return c.entity.Draw()
})
if err != nil {
log.Printf("draw with timeout error: %v\n", err)
}
}
func (c *CommandHandler) UpdateWithTimeout() {
err := channels.RunWithTimeout(c.timeout, func() error {
return c.entity.Update()
})
if err != nil {
log.Printf("update with timeout error: %v\n", err)
}
}

View File

@ -0,0 +1,8 @@
package types
import "github.com/veandco/go-sdl2/sdl"
type EntityAnimation interface {
Draw(step int32, windowPosition *sdl.Point) error
GetSpeed() int32
}

View File

@ -1,23 +1,20 @@
package entity
package types
import (
"context"
"log"
"math"
"time"
"sync"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/channels"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity/animation"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/vector"
"github.com/veandco/go-sdl2/sdl"
)
type penguin struct {
ctx context.Context
timeout time.Duration
commandChan chan EntityCommand
mx sync.RWMutex
// Animation parameters
currentAnimation *entityAnimation
currentAnimation EntityAnimation
animationStep int32
facingRight bool
updateAnimation bool
@ -28,20 +25,13 @@ type penguin struct {
speed float64
}
func NewPenguin(ctx context.Context, renderer *sdl.Renderer) *penguin {
commandChan := make(chan EntityCommand, 10)
defaultTimeout := time.Second
func NewPenguin(renderer *sdl.Renderer) *penguin {
position := vector.Vec2F{}
direction := vector.Vec2F{}
speed := 1.0
p := penguin{
ctx: ctx,
commandChan: commandChan,
timeout: defaultTimeout,
currentAnimation: penguinAnimations[PENGUIN_DEFAULT],
currentAnimation: animation.PenguinAnimations[animation.PENGUIN_DEFAULT],
animationStep: 0,
facingRight: true,
@ -54,8 +44,9 @@ func NewPenguin(ctx context.Context, renderer *sdl.Renderer) *penguin {
}
func (p *penguin) Draw() error {
step := p.animationStep / p.currentAnimation.speed
step := p.animationStep / p.currentAnimation.GetSpeed()
// TODO
//windowPosition := worldPosToWindowPos()
windowPosition := &sdl.Point{
X: int32(math.Round(p.worldPosition.X)),
@ -77,11 +68,11 @@ func (p *penguin) SetPosition(vec *vector.Vec2F) {
}
func (p *penguin) SetAnimation(id int) {
a, exists := penguinAnimations[id]
a, exists := animation.PenguinAnimations[id]
if !exists {
log.Printf("animation does not exist: %v", id)
a = penguinAnimations[PENGUIN_DEFAULT]
a = animation.PenguinAnimations[animation.PENGUIN_DEFAULT]
}
if a != p.currentAnimation {
@ -92,12 +83,18 @@ func (p *penguin) SetAnimation(id int) {
}
func (p *penguin) MoveX(x float64) {
p.mx.Lock()
defer p.mx.Unlock()
p.direction.X = x
p.direction.Normalize()
p.updateAnimation = true
}
func (p *penguin) MoveY(y float64) {
p.mx.Lock()
defer p.mx.Unlock()
// (0,0) is the top left, so negative y moves up
p.direction.Y = y
p.direction.Normalize()
@ -119,9 +116,9 @@ func (p *penguin) SetMoveAnimation() {
if x == 0 && y == 0 {
// Stationary
if p.facingRight {
p.SetAnimation(PENGUIN_STATIONARY_RIGHT)
p.SetAnimation(animation.PENGUIN_STATIONARY_RIGHT)
} else {
p.SetAnimation(PENGUIN_STATIONARY_LEFT)
p.SetAnimation(animation.PENGUIN_STATIONARY_LEFT)
}
} else {
// Moving
@ -136,9 +133,9 @@ func (p *penguin) SetMoveAnimation() {
// if x == 0, stay facing whatever direction we were previously facing
if p.facingRight {
p.SetAnimation(PENGUIN_WALK_RIGHT)
p.SetAnimation(animation.PENGUIN_WALK_RIGHT)
} else {
p.SetAnimation(PENGUIN_WALK_LEFT)
p.SetAnimation(animation.PENGUIN_WALK_LEFT)
}
}
}
@ -157,62 +154,3 @@ func (p *penguin) Update() error {
return nil
}
func (p *penguin) GetCommandChan() chan EntityCommand {
return p.commandChan
}
func (p *penguin) Run() {
running := true
for running {
select {
case c := <-p.commandChan:
go func(cmd EntityCommand) {
p.HandleWithTimeout(cmd)
}(c)
case <-p.ctx.Done():
// Graceful shutdown
running = false
// Finish up anything in the queue
for c := range p.commandChan {
p.HandleWithTimeout(c)
}
log.Printf("entity shutdown\n")
}
}
}
func (p *penguin) HandleWithTimeout(c EntityCommand) {
err := channels.RunWithTimeout(p.timeout, func() error {
return p.Handle(c)
})
if err != nil {
log.Printf("%v\n", err)
}
}
func (p *penguin) Handle(c EntityCommand) error {
var err error
switch c.key {
case COMMAND_DRAW:
err = p.Draw()
if err != nil {
log.Printf("error drawing entity: %v", err)
}
case COMMAND_UPDATE:
err = p.Update()
if err != nil {
log.Printf("error updating entity: %v", err)
}
case COMMAND_MOVE_X:
p.MoveX(c.value)
case COMMAND_MOVE_Y:
p.MoveY(c.value)
case COMMAND_SET_SPEED:
p.SetSpeed(c.value)
default:
log.Printf("unknown entity command: %v", c.key)
}
return nil
}

View File

@ -4,22 +4,28 @@ import (
"context"
"fmt"
"log"
"math/rand"
"strconv"
"sync"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity/player"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity/animation"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity/command"
"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/vector"
"gitea.wisellama.rocks/Wisellama/gosimpleconf"
"github.com/veandco/go-sdl2/sdl"
)
type Entity interface {
type EntityCmdHandler interface {
Run()
GetCommandChan() chan entity.EntityCommand
Draw() error
Update() error
CloseRequests()
CommandRequest() chan command.EntityCommand
DrawRequest() chan bool
DrawResponse() chan bool
UpdateRequest() chan bool
UpdateResponse() chan bool
}
// Run is the main function to start the game.
@ -73,35 +79,41 @@ func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error {
}
defer sprite.CleanupSpriteCache()
entity.DefinePenguinAnimations()
animation.DefineAnimations()
// Done with main setup, now moving on to creating specific entities
entityList := make([]Entity, 0)
entityList := make([]EntityCmdHandler, 0)
wg := sync.WaitGroup{}
// Setup Player 1
// Let them control a penguin to start with
player1 := player.NewPlayer(ctx)
penguin := entity.NewPenguin(ctx, renderer)
penguin.SetSpeed(2.0)
entityList = append(entityList, penguin)
player1.SetEntityChan(penguin.GetCommandChan())
penguinEntity := types.NewPenguin(renderer)
penguinCmdHandler := command.NewCommandHandler(ctx, penguinEntity)
penguinEntity.SetSpeed(2.0)
entityList = append(entityList, penguinCmdHandler)
player1.SetEntityChan(penguinCmdHandler.CommandRequest())
wg.Add(1)
go func() {
defer wg.Done()
penguin.Run()
penguinCmdHandler.Run()
}()
p2 := entity.NewPenguin(ctx, renderer)
p2.SetPosition(&vector.Vec2F{X: 100, Y: 100})
p2.SetAnimation(entity.PENGUIN_WALK_LEFT)
entityList = append(entityList, p2)
wg.Add(1)
go func() {
defer wg.Done()
p2.Run()
}()
for i := 0; i < 10; i++ {
entity := types.NewPenguin(renderer)
entityCmd := command.NewCommandHandler(ctx, entity)
randomPos := vector.Vec2F{X: rand.Float64() * 500, Y: rand.Float64() * 500}
entity.SetPosition(&randomPos)
entity.SetAnimation(animation.PENGUIN_WALK_LEFT)
entityList = append(entityList, entityCmd)
wg.Add(1)
go func() {
defer wg.Done()
entityCmd.Run()
}()
}
// And now starting the main loop
wg.Add(1)
@ -112,15 +124,6 @@ func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error {
running := true
for running {
// Allow us to exit gracefully if the context is done
select {
case <-ctx.Done():
running = false
default:
// Keep running
}
// Poll for SDL events
var event sdl.Event
sdl.Do(func() {
@ -145,6 +148,17 @@ func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error {
}
})
// Allow us to exit early if the context is done
select {
case <-ctx.Done():
running = false
default:
// Keep running
}
if !running {
break
}
// Background
sdl.Do(func() {
err = renderer.SetDrawColor(0, 120, 0, 255)
@ -158,19 +172,16 @@ func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error {
}
})
// Everything else
// Tell everything to Update and Draw
for _, e := range entityList {
//e.GetCommandChan() <- entity.NewEntityCommand(entity.COMMAND_UPDATE, 0)
//e.GetCommandChan() <- entity.NewEntityCommand(entity.COMMAND_DRAW, 0)
err = e.Update()
if err != nil {
log.Printf("error updating: %v", err)
}
e.UpdateRequest() <- true
e.DrawRequest() <- true
}
err = e.Draw()
if err != nil {
log.Printf("error drawing: %v", err)
}
// Wait for each entity to finish their Draw and Update commands before proceeding
for _, e := range entityList {
<-e.UpdateResponse()
<-e.DrawResponse()
}
// Draw
@ -180,9 +191,10 @@ func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error {
})
}
close(player1.GetSdlEventsChan())
player1.Cleanup()
for _, e := range entityList {
close(e.GetCommandChan())
e.CloseRequests()
}
wg.Wait()

View File

@ -3,10 +3,11 @@ 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"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity/command"
"github.com/veandco/go-sdl2/sdl"
)
@ -15,41 +16,55 @@ import (
// 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 {
ctx context.Context
timeout time.Duration
sdlEventsChan chan sdl.KeyboardEvent
mx sync.RWMutex
ctx context.Context
timeout time.Duration
sdlKeyboardEventsChan chan sdl.KeyboardEvent
entityChan chan entity.EntityCommand
entityChan chan command.EntityCommand
keystates map[sdl.Keycode]bool
}
func NewPlayer(ctx context.Context) *player {
sdlEventsChan := make(chan sdl.KeyboardEvent, 10)
sdlKeyboardEventsChan := make(chan sdl.KeyboardEvent, 10)
defaultTimeout := time.Second
keystates := make(map[sdl.Keycode]bool)
p := player{
ctx: ctx,
timeout: defaultTimeout,
sdlEventsChan: sdlEventsChan,
keystates: keystates,
ctx: ctx,
timeout: defaultTimeout,
sdlKeyboardEventsChan: sdlKeyboardEventsChan,
keystates: keystates,
}
return &p
}
func (p *player) SetEntityChan(e chan entity.EntityCommand) {
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()
p.entityChan = e
}
func (p *player) GetSdlEventsChan() chan sdl.KeyboardEvent {
return p.sdlEventsChan
p.mx.RLock()
defer p.mx.RUnlock()
return p.sdlKeyboardEventsChan
}
func (p *player) Run() {
running := true
for running {
select {
case e := <-p.sdlEventsChan:
case e := <-p.sdlKeyboardEventsChan:
go func(event sdl.KeyboardEvent) {
p.HandleWithTimeout(event)
}(e)
@ -57,10 +72,9 @@ func (p *player) Run() {
// Graceful shutdown
running = false
// Finish up anything in the queue
for e := range p.sdlEventsChan {
for e := range p.sdlKeyboardEventsChan {
p.HandleWithTimeout(e)
}
log.Printf("player shutdown\n")
}
}
}
@ -75,7 +89,9 @@ func (p *player) HandleWithTimeout(event sdl.KeyboardEvent) {
}
func (p *player) Handle(e sdl.KeyboardEvent) error {
log.Printf("%v", e)
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 {
@ -97,27 +113,27 @@ func (p *player) Handle(e sdl.KeyboardEvent) error {
// Speed
if p.keystates[sdl.K_LSHIFT] {
p.entityChan <- entity.NewEntityCommand(entity.COMMAND_SET_SPEED, 4)
p.entityChan <- command.NewEntityCommand(command.SET_SPEED, 4)
} else {
p.entityChan <- entity.NewEntityCommand(entity.COMMAND_SET_SPEED, 2)
p.entityChan <- command.NewEntityCommand(command.SET_SPEED, 2)
}
// Move X
if p.keystates[sdl.K_d] {
p.entityChan <- entity.NewEntityCommand(entity.COMMAND_MOVE_X, 1.0)
p.entityChan <- command.NewEntityCommand(command.MOVE_X, 1.0)
} else if p.keystates[sdl.K_a] {
p.entityChan <- entity.NewEntityCommand(entity.COMMAND_MOVE_X, -1.0)
p.entityChan <- command.NewEntityCommand(command.MOVE_X, -1.0)
} else if !p.keystates[sdl.K_d] && !p.keystates[sdl.K_a] {
p.entityChan <- entity.NewEntityCommand(entity.COMMAND_MOVE_X, 0.0)
p.entityChan <- command.NewEntityCommand(command.MOVE_X, 0.0)
}
// Move Y
if p.keystates[sdl.K_w] {
p.entityChan <- entity.NewEntityCommand(entity.COMMAND_MOVE_Y, -1.0)
p.entityChan <- command.NewEntityCommand(command.MOVE_Y, -1.0)
} else if p.keystates[sdl.K_s] {
p.entityChan <- entity.NewEntityCommand(entity.COMMAND_MOVE_Y, 1.0)
p.entityChan <- command.NewEntityCommand(command.MOVE_Y, 1.0)
} else if !p.keystates[sdl.K_w] && !p.keystates[sdl.K_s] {
p.entityChan <- entity.NewEntityCommand(entity.COMMAND_MOVE_Y, 0.0)
p.entityChan <- command.NewEntityCommand(command.MOVE_Y, 0.0)
}
return nil
}