Split off each entity as a separate go routine

main
Sean Hickey 2022-10-22 02:00:29 -07:00
parent a3dc1b7b5c
commit b148699a58
11 changed files with 505 additions and 174 deletions

View File

@ -1,4 +1,5 @@
game.title = "Project Ely"
game.framerate = 60.0
log.file = "output.log"
log.writeToFile = false

View File

@ -0,0 +1,29 @@
package channels
import (
"context"
"time"
)
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
}
return err
}

View File

@ -8,6 +8,7 @@ import (
var defaultConfig gosimpleconf.ConfigMap = gosimpleconf.ConfigMap{
"game.title": "Project Ely",
"game.framerate": "60.0",
"log.writeToFile": "false",
}

View File

@ -0,0 +1,22 @@
package entity
// List of keys supported by all entities
const (
COMMAND_DRAW int = iota
COMMAND_UPDATE
COMMAND_MOVE_X
COMMAND_MOVE_Y
COMMAND_SET_SPEED
)
type EntityCommand struct {
key int
value float64
}
func NewEntityCommand(key int, value float64) EntityCommand {
return EntityCommand{
key: key,
value: value,
}
}

View File

@ -43,3 +43,7 @@ func NewEntityAnimation(
func (e *entityAnimation) Draw(frame int32, windowPosition *sdl.Point) error {
return e.spriteAnimation.Draw(frame, windowPosition, e.angle, e.center, e.flip)
}
func DefineAnimations() {
DefinePenguinAnimations()
}

View File

@ -1,14 +1,21 @@
package entity
import (
"context"
"log"
"math"
"time"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/channels"
"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
// Animation parameters
currentAnimation *entityAnimation
animationStep int32
@ -21,17 +28,25 @@ type penguin struct {
speed float64
}
func NewPenguin(renderer *sdl.Renderer) *penguin {
func NewPenguin(ctx context.Context, renderer *sdl.Renderer) *penguin {
commandChan := make(chan EntityCommand, 10)
defaultTimeout := time.Second
position := vector.Vec2F{}
direction := vector.Vec2F{}
speed := 1.0
p := penguin{
ctx: ctx,
commandChan: commandChan,
timeout: defaultTimeout,
currentAnimation: penguinAnimations[PENGUIN_DEFAULT],
animationStep: 0,
facingRight: true,
worldPosition: &position,
speed: 2.0,
speed: speed,
direction: &direction,
}
@ -61,11 +76,11 @@ func (p *penguin) SetPosition(vec *vector.Vec2F) {
p.worldPosition = vec
}
func (p *penguin) SetAnimation(name string) {
a, exists := penguinAnimations[name]
func (p *penguin) SetAnimation(id int) {
a, exists := penguinAnimations[id]
if !exists {
log.Printf("animation does not exist: %v", name)
log.Printf("animation does not exist: %v", id)
a = penguinAnimations[PENGUIN_DEFAULT]
}
@ -78,15 +93,25 @@ func (p *penguin) SetAnimation(name string) {
func (p *penguin) MoveX(x float64) {
p.direction.X = x
p.direction.Normalize()
p.updateAnimation = true
}
func (p *penguin) MoveY(y float64) {
// (0,0) is the top left, so negative y moves up
p.direction.Y = y
p.direction.Normalize()
p.updateAnimation = true
}
func (p *penguin) SetSpeed(s float64) {
p.speed = s
}
func (p *penguin) GetSpeed() float64 {
return p.speed
}
func (p *penguin) SetMoveAnimation() {
x := p.direction.X
y := p.direction.Y
@ -124,12 +149,70 @@ func (p *penguin) Update() error {
p.updateAnimation = false
}
p.direction.Normalize()
x := p.direction.X * p.speed
y := p.direction.Y * p.speed
log.Printf("X: %v, Y: %v\n", x, y)
p.worldPosition.X += x
p.worldPosition.Y += y
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

@ -5,13 +5,13 @@ import (
"github.com/veandco/go-sdl2/sdl"
)
var penguinAnimations map[string]*entityAnimation
var penguinAnimations map[int]*entityAnimation
const (
PENGUIN_WALK_RIGHT = "walk-right"
PENGUIN_WALK_LEFT = "walk-left"
PENGUIN_STATIONARY_RIGHT = "stationary-right"
PENGUIN_STATIONARY_LEFT = "stationary-left"
PENGUIN_WALK_RIGHT int = iota
PENGUIN_WALK_LEFT
PENGUIN_STATIONARY_RIGHT
PENGUIN_STATIONARY_LEFT
PENGUIN_DEFAULT = PENGUIN_STATIONARY_RIGHT
)
@ -28,7 +28,7 @@ func DefinePenguinAnimations() {
)
dimensions = sdl.Point{X: 32, Y: 32}
penguinAnimations = make(map[string]*entityAnimation)
penguinAnimations = make(map[int]*entityAnimation)
// Walking Right is in the spritesheet.
speed = 5

View File

@ -0,0 +1,123 @@
package player
import (
"context"
"log"
"time"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/channels"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity"
"github.com/veandco/go-sdl2/sdl"
)
// 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 {
ctx context.Context
timeout time.Duration
sdlEventsChan chan sdl.KeyboardEvent
entityChan chan entity.EntityCommand
keystates map[sdl.Keycode]bool
}
func NewPlayer(ctx context.Context) *player {
sdlEventsChan := make(chan sdl.KeyboardEvent, 10)
defaultTimeout := time.Second
keystates := make(map[sdl.Keycode]bool)
p := player{
ctx: ctx,
timeout: defaultTimeout,
sdlEventsChan: sdlEventsChan,
keystates: keystates,
}
return &p
}
func (p *player) SetEntityChan(e chan entity.EntityCommand) {
p.entityChan = e
}
func (p *player) GetSdlEventsChan() chan sdl.KeyboardEvent {
return p.sdlEventsChan
}
func (p *player) Run() {
running := true
for running {
select {
case e := <-p.sdlEventsChan:
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.sdlEventsChan {
p.HandleWithTimeout(e)
}
log.Printf("player shutdown\n")
}
}
}
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 {
log.Printf("%v", e)
// 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
}
// Speed
if p.keystates[sdl.K_LSHIFT] {
p.entityChan <- entity.NewEntityCommand(entity.COMMAND_SET_SPEED, 4)
} else {
p.entityChan <- entity.NewEntityCommand(entity.COMMAND_SET_SPEED, 2)
}
// Move X
if p.keystates[sdl.K_d] {
p.entityChan <- entity.NewEntityCommand(entity.COMMAND_MOVE_X, 1.0)
} else if p.keystates[sdl.K_a] {
p.entityChan <- entity.NewEntityCommand(entity.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)
}
// Move Y
if p.keystates[sdl.K_w] {
p.entityChan <- entity.NewEntityCommand(entity.COMMAND_MOVE_Y, -1.0)
} else if p.keystates[sdl.K_s] {
p.entityChan <- entity.NewEntityCommand(entity.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)
}
return nil
}

191
internal/game/game.go Normal file
View File

@ -0,0 +1,191 @@
package game
import (
"context"
"fmt"
"log"
"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/sprite"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/vector"
"gitea.wisellama.rocks/Wisellama/gosimpleconf"
"github.com/veandco/go-sdl2/sdl"
)
type Entity interface {
Run()
GetCommandChan() chan entity.EntityCommand
Draw() error
Update() error
}
// Run is the main function to start the game.
func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error {
var err error
ctx, cancel := context.WithCancel(ctx)
defer cancel()
framerate, err := strconv.ParseFloat(configMap["game.framerate"], 64)
if err != nil {
err = fmt.Errorf("error parsing framerate: %w", err)
return err
}
framerateDelay := uint32(1000 / framerate)
err = SdlInit()
if err != nil {
return err
}
defer SdlQuit()
gameWindow, err := NewWindow(configMap["game.title"])
if err != nil {
err = fmt.Errorf("failed creating GameWindow: %w", err)
return err
}
defer gameWindow.Cleanup()
var renderer *sdl.Renderer
sdl.Do(func() {
renderer, err = sdl.CreateRenderer(gameWindow.SdlWindow, -1, sdl.RENDERER_ACCELERATED)
})
if err != nil {
err = fmt.Errorf("failed creating SDL renderer: %w", err)
return err
}
defer func() {
sdl.Do(func() {
err = renderer.Destroy()
if err != nil {
log.Printf("error destroying renderer: %v\n", err)
}
})
}()
err = sprite.InitSpriteCache(renderer)
if err != nil {
err = fmt.Errorf("failed in InitSpriteCache: %w", err)
return err
}
defer sprite.CleanupSpriteCache()
entity.DefinePenguinAnimations()
// Done with main setup, now moving on to creating specific entities
entityList := make([]Entity, 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())
wg.Add(1)
go func() {
defer wg.Done()
penguin.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()
}()
// And now starting the main loop
wg.Add(1)
go func() {
defer wg.Done()
player1.Run()
}()
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() {
event = sdl.PollEvent()
for event != nil {
switch e := event.(type) {
case *sdl.QuitEvent:
log.Println("QuitEvent quitting")
cancel()
case *sdl.KeyboardEvent:
if e.Keysym.Sym == sdl.K_ESCAPE {
log.Println("Esc quitting")
cancel()
} else {
// Publish the event so other components can do whatever they need with it
player1.GetSdlEventsChan() <- *e
}
}
event = sdl.PollEvent()
}
})
// Background
sdl.Do(func() {
err = renderer.SetDrawColor(0, 120, 0, 255)
if err != nil {
log.Printf("error in renderer.SetDrawColor: %v\n", err)
}
err = renderer.Clear()
if err != nil {
log.Printf("error in renderer.Clear: %v\n", err)
}
})
// Everything else
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)
}
err = e.Draw()
if err != nil {
log.Printf("error drawing: %v", err)
}
}
// Draw
sdl.Do(func() {
renderer.Present()
sdl.Delay(framerateDelay)
})
}
close(player1.GetSdlEventsChan())
for _, e := range entityList {
close(e.GetCommandChan())
}
wg.Wait()
return nil
}

View File

@ -1,13 +0,0 @@
package game
// 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 {
}
func NewPlayer() *player {
p := player{}
return &p
}

186
main.go
View File

@ -1,20 +1,53 @@
package main
import (
"fmt"
"context"
"log"
"os"
"os/signal"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/config"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity"
"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"
)
func main() {
// Setup some initial context for gracefully killing
// the program with system interrupt signals like ctrl+c
// https://pace.dev/blog/2020/02/17/repond-to-ctrl-c-interrupt-signals-gracefully-with-context-in-golang-by-mat-ryer.html
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
defer func() {
signal.Stop(signalChan)
cancel()
}()
go func() {
select {
case <-signalChan:
// Graceful exit on ctrl+c
log.Printf("Attempting to exit gracefully...\n")
cancel()
case <-ctx.Done():
}
<-signalChan // Hard exit on second ctrl+c
log.Printf("Hard kill\n")
os.Exit(2)
}()
// Run the program
err := run(ctx)
if err != nil {
log.Printf("%v\n", err)
os.Exit(1)
}
log.Printf("Exited gracefully!\n")
}
func run(ctx context.Context) error {
var err error
// Read configuration
@ -35,155 +68,12 @@ func main() {
// Start everything with the SDL goroutine context
log.Printf("=== Starting %v ===", configMap["game.title"])
sdl.Main(func() {
err = run(configMap)
err = game.Run(ctx, configMap)
})
exitcode := 0
if err != nil {
log.Printf("ERROR: %v\n", err)
exitcode = 1
}
os.Exit(exitcode)
}
func run(configMap gosimpleconf.ConfigMap) error {
var err error
err = game.SdlInit()
if err != nil {
return err
}
defer game.SdlQuit()
gameWindow, err := game.NewWindow(configMap["game.title"])
if err != nil {
err = fmt.Errorf("failed creating GameWindow: %w", err)
return err
}
defer gameWindow.Cleanup()
var renderer *sdl.Renderer
sdl.Do(func() {
renderer, err = sdl.CreateRenderer(gameWindow.SdlWindow, -1, sdl.RENDERER_ACCELERATED)
})
if err != nil {
err = fmt.Errorf("failed creating SDL renderer: %w", err)
return err
}
defer func() {
sdl.Do(func() {
err = renderer.Destroy()
if err != nil {
log.Printf("error destroying renderer: %v\n", err)
}
})
}()
err = sprite.InitSpriteCache(renderer)
if err != nil {
err = fmt.Errorf("failed in InitSpriteCache: %w", err)
return err
}
defer sprite.CleanupSpriteCache()
entity.DefinePenguinAnimations()
penguin := entity.NewPenguin(renderer)
p2 := entity.NewPenguin(renderer)
p2.SetPosition(&vector.Vec2F{X: 100, Y: 100})
p2.SetAnimation(entity.PENGUIN_WALK_LEFT)
keystates := make(map[sdl.Keycode]bool)
running := true
for running {
var event sdl.Event
sdl.Do(func() {
event = sdl.PollEvent()
})
for event != nil {
switch e := event.(type) {
case *sdl.QuitEvent:
running = false
case *sdl.KeyboardEvent:
if e.Keysym.Sym == sdl.K_ESCAPE {
log.Println("Esc quitting")
running = false
}
// Key states (just set a boolean whether the key is actively being pressed)
if e.Type == sdl.KEYDOWN {
keystates[e.Keysym.Sym] = true
} else if e.Type == sdl.KEYUP {
keystates[e.Keysym.Sym] = false
}
}
sdl.Do(func() {
event = sdl.PollEvent()
})
}
if keystates[sdl.K_d] {
penguin.MoveX(1)
} else if keystates[sdl.K_a] {
penguin.MoveX(-1)
} else {
penguin.MoveX(0)
}
if keystates[sdl.K_w] {
penguin.MoveY(-1)
} else if keystates[sdl.K_s] {
penguin.MoveY(1)
} else {
penguin.MoveY(0)
}
if !keystates[sdl.K_a] && !keystates[sdl.K_d] && !keystates[sdl.K_w] && !keystates[sdl.K_s] {
penguin.MoveX(0)
penguin.MoveY(0)
}
// Background
sdl.Do(func() {
err = renderer.SetDrawColor(0, 120, 0, 255)
if err != nil {
log.Printf("error in renderer.SetDrawColor: %v\n", err)
}
err = renderer.Clear()
if err != nil {
log.Printf("error in renderer.Clear: %v\n", err)
}
})
// Everything else
err = penguin.Update()
if err != nil {
log.Printf("error updating: %v\n", err)
}
err = penguin.Draw()
if err != nil {
log.Printf("error drawing: %v\n", err)
}
err = p2.Update()
if err != nil {
log.Printf("error updating: %v\n", err)
}
err = p2.Draw()
if err != nil {
log.Printf("error drawing: %v\n", err)
}
// Draw
sdl.Do(func() {
renderer.Present()
sdl.Delay(16) // 60 FPS, well ok 62.5 FPS
})
}
return nil
}