Move all engine stuff to GoRetro. Still many interactions between game and engine code that there should not be.

This commit is contained in:
stevenhowes
2022-01-03 22:13:34 +00:00
commit 4d1ab58ca4
19 changed files with 976 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
package GoRetro
/*
* --------------------
* Animator
* --------------------
* Handles sprites and their associated animation sequences
*/
import (
"fmt"
"io/ioutil"
"path"
"time"
"github.com/veandco/go-sdl2/sdl"
)
type animator struct {
container *Element
sequences map[string]*Sequence // All of the available sequences
current string // The current sequence
lastFrameChange time.Time // When we last ticked between frames
finished bool // We're on the last frame
}
func NewAnimator(
container *Element,
sequences map[string]*Sequence,
defaultSequence string) *animator {
var an animator
an.container = container
an.sequences = sequences
an.current = defaultSequence
an.lastFrameChange = time.Now()
return &an
}
func (an *animator) onUpdate() error {
sequence := an.sequences[an.current]
// Calculate time per frame
frameInterval := float64(time.Second) / sequence.sampleRate
// If we've exceeded that time since the last frame, bump it along one
if time.Since(an.lastFrameChange) >= time.Duration(frameInterval) {
an.finished = sequence.nextFrame()
an.lastFrameChange = time.Now()
}
return nil
}
func (an *animator) onDraw() error {
tex := an.sequences[an.current].texture()
return drawTexture(
tex,
an.container.Position,
an.container.Rotation,
an.container.Renderer)
}
func (an *animator) onCollision(other *Element) error {
return nil
}
func (an *animator) setSequence(name string) bool {
_, ok := an.sequences[name]
if ok {
// If we *are* changing sequence, change the name and reset the frame
if an.current != name {
// Reset the old sequence to frame 0
sequence := an.sequences[an.current]
sequence.resetFrame()
// Use the new sequence
an.current = name
an.lastFrameChange = time.Now()
}
}
return ok
}
type Sequence struct {
textures []*sdl.Texture // The frames
frame int // Current frame
sampleRate float64 // Frames per second
loop bool // Does this sequence play continuously?
}
func NewSequence(
filepath string, // Path to the folder for this sequence
sampleRate float64,
loop bool,
renderer *sdl.Renderer) (*Sequence, error) {
var seq Sequence
// Get a list of frames
files, err := ioutil.ReadDir(filepath)
if err != nil {
return nil, fmt.Errorf("reading directory %v: %v", filepath, err)
}
for _, file := range files {
filename := path.Join(filepath, file.Name())
// Load this frame and turn it into a texture
tex, err := loadTextureFromBMP(filename, renderer)
if err != nil {
return nil, fmt.Errorf("loading sequence frame: %v", err)
}
seq.textures = append(seq.textures, tex)
}
seq.sampleRate = sampleRate
seq.loop = loop
return &seq, nil
}
func (seq *Sequence) texture() *sdl.Texture {
return seq.textures[seq.frame]
}
func (seq *Sequence) resetFrame() {
seq.frame = 0
}
func (seq *Sequence) nextFrame() bool {
// If we're at the end
if seq.frame == len(seq.textures)-1 {
if seq.loop {
// Reset for a looping sequence
seq.resetFrame()
} else {
return true
}
} else {
seq.frame++
}
return false
}
+39
View File
@@ -0,0 +1,39 @@
package GoRetro
/*
* --------------------
* BounderScreen
* --------------------
* A bounder which sets active = false on anything which
* has left the screen
*/
type bounderScreen struct {
container *Element
}
func NewBounderScreen(container *Element) *bounderScreen {
return &bounderScreen{container: container}
}
func (bounder *bounderScreen) onDraw() error {
return nil
}
func (bounder *bounderScreen) onUpdate() error {
b := bounder.container
// If the position is outside the screen bounds then set it as inactive
// and mark for deletion
if b.Position.X > ScreenWidth || b.Position.X < 0 ||
b.Position.Y > ScreenHeight || b.Position.Y < 0 {
b.Active = false
b.Delete = true
}
return nil
}
func (bounder *bounderScreen) onCollision(other *Element) error {
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package GoRetro
/*
* --------------------
* BounderScreen
* --------------------
* A bounder which resets the position to the opposite of
* whichever bound was hit
*/
type bounderScreenResetting struct {
container *Element
}
func NewBounderScreenResetting(container *Element) *bounderScreenResetting {
return &bounderScreenResetting{container: container}
}
func (bounder *bounderScreenResetting) onDraw() error {
return nil
}
func (bounder *bounderScreenResetting) onUpdate() error {
b := bounder.container
// If any position exceeds the screen dimensions, wrap it to the
// opposite side
if b.Position.X > ScreenWidth {
b.Position.X = 0
}
if b.Position.X < 0 {
b.Position.X = ScreenWidth
}
if b.Position.Y > ScreenHeight {
b.Position.Y = 0
}
if b.Position.Y < 0 {
b.Position.Y = ScreenWidth
}
return nil
}
func (bounder *bounderScreenResetting) onCollision(other *Element) error {
return nil
}
+58
View File
@@ -0,0 +1,58 @@
package GoRetro
/*
* --------------------
* damageGiver
* --------------------
* During a collision a damageReciever handles damage from
* a damageGiver
*/
type damageGiver struct {
container *Element
damage float64 // Damage per incident (or tick if damagePerists)
damageActive bool // Is currently able to issue damage
damagePersists bool // Can this continue to damage once it's been hit
}
func NewDamageGiver(container *Element, damage float64, damagePersists bool) *damageGiver {
return &damageGiver{
container: container,
damage: damage,
damageActive: true,
damagePersists: damagePersists,
}
}
func (dg *damageGiver) onDraw() error {
return nil
}
func (dg *damageGiver) onUpdate() error {
if dg.container.checkComponentIsPresent(&damageReceiver{}) {
dr := dg.container.getComponent(&damageReceiver{}).(*damageReceiver)
if dr.health <= 0 {
// We can't give damage any more if our reciever is at 0 (i.e. container is dead)
dg.damageActive = false
}
}
return nil
}
func (dg *damageGiver) onCollision(other *Element) error {
if !dg.damageActive {
return nil
}
if other.checkComponentIsPresent(&damageReceiver{}) {
// Find our victim and subtract damage
dr := other.getComponent(&damageReceiver{}).(*damageReceiver)
dr.health -= dg.damage
// If we don't continue to hand out damage then disable ourselves
if !dg.damagePersists {
dg.damageActive = false
}
}
return nil
}
+69
View File
@@ -0,0 +1,69 @@
package GoRetro
/*
* --------------------
* damageReceiver
* --------------------
* During a collision a damageReciever handles damage from
* a damageGiver
*/
type damageReceiver struct {
container *Element
health float64
}
func NewDamageReceiver(container *Element, health float64) *damageReceiver {
return &damageReceiver{
container: container,
health: health,
}
}
func (dr *damageReceiver) onDraw() error {
return nil
}
func (dr *damageReceiver) onUpdate() error {
// If we're out of health
if dr.health <= 0 {
// If we have an animator, run the destroy sequence
if dr.container.checkComponentIsPresent(&animator{}) {
ani := dr.container.getComponent(&animator{}).(*animator)
// If the sequence can't get set (doesn't exist) just remove us
if !ani.setSequence("destroy") {
dr.container.Active = false
dr.container.Delete = true
}
}
// Stop any movers we have
if dr.container.checkComponentIsPresent(&moverLinear{}) {
lm := dr.container.getComponent(&moverLinear{}).(*moverLinear)
lm.speed = 0
}
if dr.container.checkComponentIsPresent(&moverKeyboard{}) {
km := dr.container.getComponent(&moverKeyboard{}).(*moverKeyboard)
km.speed = 0
}
}
// If we've finished our destroy animation then we're not active and can be removed
if dr.container.checkComponentIsPresent(&animator{}) {
ani := dr.container.getComponent(&animator{}).(*animator)
if ani.finished && ani.current == "destroy" {
dr.container.Active = false
dr.container.Delete = true
}
}
/*if debugTick {
fmt.Printf("health is %f\n", dr.health)
}*/
return nil
}
func (dr *damageReceiver) onCollision(other *Element) error {
return nil
}
+90
View File
@@ -0,0 +1,90 @@
package GoRetro
/*
* --------------------
* moverKeyboard
* --------------------
* A simple mover that moves the container a fixed
* distance each tick when arrow keys are used
*
* NOTE: Container must have a spriteRenderer to
* read dimesions from!
*/
import (
"github.com/veandco/go-sdl2/sdl"
)
type moverKeyboard struct {
container *Element
speed float64
}
func NewMoverKeyboard(container *Element, speed float64) *moverKeyboard {
return &moverKeyboard{
container: container,
speed: speed,
}
}
func (mover *moverKeyboard) onDraw() error {
return nil
}
func (mover *moverKeyboard) onUpdate() error {
keys := sdl.GetKeyboardState()
// For now, spoof a 1 radius circle above, below, left and right of the player
// to keep them within the screen. bounder_screen would make the player cease
// to exist if we did that
cLeft := Circle{
Radius: 1,
Center: Vector{X: 0, Y: mover.container.Position.Y},
}
cRight := Circle{
Radius: 1,
Center: Vector{X: ScreenWidth, Y: mover.container.Position.Y},
}
cTop := Circle{
Radius: 1,
Center: Vector{X: mover.container.Position.X, Y: 0},
}
cBottom := Circle{
Radius: 1,
Center: Vector{X: mover.container.Position.X, Y: ScreenHeight},
}
// Handle direction keys and check of we collide.
if keys[sdl.SCANCODE_LEFT] == 1 {
for _, c2 := range mover.container.Collisions {
if !collides(cLeft, circleOffset(c2, mover.container.Position)) {
mover.container.Position.X -= mover.speed * Delta
}
}
} else if keys[sdl.SCANCODE_RIGHT] == 1 {
for _, c2 := range mover.container.Collisions {
if !collides(cRight, circleOffset(c2, mover.container.Position)) {
mover.container.Position.X += mover.speed * Delta
}
}
}
if keys[sdl.SCANCODE_UP] == 1 {
for _, c2 := range mover.container.Collisions {
if !collides(cTop, circleOffset(c2, mover.container.Position)) {
mover.container.Position.Y -= mover.speed * Delta
}
}
} else if keys[sdl.SCANCODE_DOWN] == 1 {
for _, c2 := range mover.container.Collisions {
if !collides(cBottom, circleOffset(c2, mover.container.Position)) {
mover.container.Position.Y += mover.speed * Delta
}
}
}
return nil
}
func (mover *moverKeyboard) onCollision(other *Element) error {
return nil
}
+40
View File
@@ -0,0 +1,40 @@
package GoRetro
/*
* --------------------
* moverLinear
* --------------------
* A simple mover that moves the container a fixed
* distance each tick in the direction of its rotation
*/
import (
"math"
)
type moverLinear struct {
container *Element
speed float64
}
func NewMoverLinear(container *Element, speed float64) *moverLinear {
return &moverLinear{container: container, speed: speed}
}
func (mover *moverLinear) onDraw() error {
return nil
}
func (mover *moverLinear) onUpdate() error {
c := mover.container
// Move, taking into account rotation in degrees
c.Position.X += mover.speed * math.Sin(c.Rotation*(math.Pi/180)) * Delta
c.Position.Y += mover.speed * math.Cos(c.Rotation*(math.Pi/180)) * Delta
return nil
}
func (mover *moverLinear) onCollision(other *Element) error {
return nil
}
+33
View File
@@ -0,0 +1,33 @@
package GoRetro
/*
* --------------------
* moverRotator
* --------------------
* A simple mover that rotates the container a fixed
* amount each tick
*/
type moverRotator struct {
container *Element
speed float64
}
func newMoverRotator(container *Element, speed float64) *moverRotator {
return &moverRotator{container: container, speed: speed}
}
func (mover *moverRotator) onDraw() error {
return nil
}
func (mover *moverRotator) onUpdate() error {
c := mover.container
c.Rotation += mover.speed * Delta
return nil
}
func (mover *moverRotator) onCollision(other *Element) error {
return nil
}
+64
View File
@@ -0,0 +1,64 @@
package GoRetro
/*
* --------------------
* intervalShooter
* --------------------
* Fire projectiles at fixed intervals
*/
import (
"time"
"github.com/veandco/go-sdl2/sdl"
)
type intervalShooter struct {
container *Element
cooldown time.Duration // Time between shots
lastShot time.Time // Last shot
shootFunc func(renderer *sdl.Renderer, collisionLayer int) *Element
}
func NewIntervalShooter(container *Element, cooldown time.Duration, lastShot time.Time, NewBullet func(renderer *sdl.Renderer, collisionLayer int) *Element) *intervalShooter {
return &intervalShooter{
container: container,
cooldown: cooldown,
lastShot: lastShot,
shootFunc: NewBullet}
}
func (shooter *intervalShooter) onDraw() error {
return nil
}
func (shooter *intervalShooter) onUpdate() error {
//pos := shooter.container.Position
if time.Since(shooter.lastShot) >= shooter.cooldown {
// TODO: These positions should not be hard coded. Store as offset from
// container (i.e. gun positions)
shooter.shoot(shooter.container.Position.X+15, shooter.container.Position.Y-10, shooter.container.Rotation, shooter.container)
shooter.shoot(shooter.container.Position.X-15, shooter.container.Position.Y-10, shooter.container.Rotation, shooter.container)
shooter.lastShot = time.Now()
}
return nil
}
func (shooter *intervalShooter) shoot(x, y, rotation float64, parent *Element) {
bul := shooter.shootFunc(parent.Renderer, parent.CollisionLayer+1)
bul.Active = true
bul.Position.X = x
bul.Position.Y = y
bul.Rotation = rotation
bul.parentElement = parent
}
func (shooter *intervalShooter) onCollision(other *Element) error {
return nil
}
+64
View File
@@ -0,0 +1,64 @@
package GoRetro
/*
* --------------------
* keyboardShooter
* --------------------
* Fire projectiles on keypress, rate limited
*/
import (
"time"
"github.com/veandco/go-sdl2/sdl"
)
type keyboardShooter struct {
container *Element
cooldown time.Duration // Time between shots
lastShot time.Time // Last shot
shootFunc func(renderer *sdl.Renderer, collisionLayer int) *Element
}
func NewKeyboardShooter(container *Element, cooldown time.Duration, NewBullet func(renderer *sdl.Renderer, collisionLayer int) *Element) *keyboardShooter {
return &keyboardShooter{
container: container,
cooldown: cooldown,
shootFunc: NewBullet}
}
func (shooter *keyboardShooter) onDraw() error {
return nil
}
func (shooter *keyboardShooter) onUpdate() error {
keys := sdl.GetKeyboardState()
//pos := shooter.container.Position
if keys[sdl.SCANCODE_SPACE] == 1 {
if time.Since(shooter.lastShot) >= shooter.cooldown {
// TODO: These positions should not be hard coded. Store as offset from
// container (i.e. gun positions)
shooter.shoot(shooter.container.Position.X+15, shooter.container.Position.Y-10, shooter.container.Rotation, shooter.container)
shooter.shoot(shooter.container.Position.X-15, shooter.container.Position.Y-10, shooter.container.Rotation, shooter.container)
shooter.lastShot = time.Now()
}
}
return nil
}
func (shooter *keyboardShooter) shoot(x, y, rotation float64, parent *Element) {
bul := shooter.shootFunc(parent.Renderer, parent.CollisionLayer+1)
bul.Active = true
bul.Position.X = x
bul.Position.Y = y
bul.Rotation = rotation
bul.parentElement = parent
}
func (shooter *keyboardShooter) onCollision(other *Element) error {
return nil
}
+57
View File
@@ -0,0 +1,57 @@
package GoRetro
/*
* --------------------
* spriteRenderer
* --------------------
* Loads a BMP into a texture and stores dimensions
*/
import (
"fmt"
"github.com/veandco/go-sdl2/sdl"
)
type spriteRenderer struct {
container *Element
tex *sdl.Texture
width, height int
}
func NewSpriteRenderer(container *Element, renderer *sdl.Renderer, filename string) *spriteRenderer {
sr := &spriteRenderer{}
var err error
sr.tex, err = loadTextureFromBMP(filename, renderer)
if err != nil {
panic(err)
}
_, _, width, height, err := sr.tex.Query()
if err != nil {
panic(fmt.Errorf("querying texture: %v", err))
}
sr.width = int(width)
sr.height = int(height)
sr.container = container
return sr
}
func (sr *spriteRenderer) onUpdate() error {
return nil
}
func (sr *spriteRenderer) onDraw() error {
return drawTexture(
sr.tex,
sr.container.Position,
sr.container.Rotation,
sr.container.Renderer)
}
func (sr *spriteRenderer) onCollision(other *Element) error {
return nil
}
+95
View File
@@ -0,0 +1,95 @@
package GoRetro
import (
"fmt"
"reflect"
"github.com/veandco/go-sdl2/sdl"
)
type component interface {
onUpdate() error
onDraw() error
onCollision(other *Element) error
}
type Element struct {
Renderer *sdl.Renderer
Position Vector
Rotation float64
Active bool
Delete bool
Collisions []Circle
components []component
parentElement *Element
ZIndex int
CollisionLayer int
}
var Elements []*Element
func (elem *Element) Draw() error {
for _, comp := range elem.components {
err := comp.onDraw()
if err != nil {
return err
}
}
return nil
}
func (elem *Element) Update() error {
for _, comp := range elem.components {
err := comp.onUpdate()
if err != nil {
return err
}
}
return nil
}
func (elem *Element) collision(other *Element) error {
for _, comp := range elem.components {
err := comp.onCollision(other)
if err != nil {
return err
}
}
return nil
}
func (elem *Element) AddComponent(new component) {
for _, existing := range elem.components {
if reflect.TypeOf(new) == reflect.TypeOf(existing) {
panic(fmt.Sprintf(
"attempt to add new component with existing type %v",
reflect.TypeOf(new)))
}
}
elem.components = append(elem.components, new)
}
func (elem *Element) getComponent(withType component) component {
typ := reflect.TypeOf(withType)
for _, comp := range elem.components {
if reflect.TypeOf(comp) == typ {
return comp
}
}
panic(fmt.Sprintf("no component with type %v", reflect.TypeOf(withType)))
}
func (elem *Element) checkComponentIsPresent(withType component) bool {
typ := reflect.TypeOf(withType)
for _, comp := range elem.components {
if reflect.TypeOf(comp) == typ {
return true
}
}
return false
}
+5
View File
@@ -0,0 +1,5 @@
module github.com/stevenhowes/GoRetro
go 1.17
require github.com/veandco/go-sdl2 v0.4.10
+2
View File
@@ -0,0 +1,2 @@
github.com/veandco/go-sdl2 v0.4.10 h1:8QoD2bhWl7SbQDflIAUYWfl9Vq+mT8/boJFAUzAScgY=
github.com/veandco/go-sdl2 v0.4.10/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
+17
View File
@@ -0,0 +1,17 @@
package GoRetro
import "time"
const (
ScreenWidth = 1024
ScreenHeight = 768
TargetTicksPerSecond = 60
DebugStatePrintSeconds = 1
dataDir = "data/"
)
var Delta float64
var LastDebugStatePrint time.Time
var DebugTick bool
+12
View File
@@ -0,0 +1,12 @@
package GoRetro
func absInt(x int) int {
return absDiffInt(x, 0)
}
func absDiffInt(x, y int) int {
if x < y {
return y - x
}
return x - y
}
+65
View File
@@ -0,0 +1,65 @@
package GoRetro
// Anything > 1 apart will collide (i.e. player wont collide with own projectile)
const (
_ = 0
LayerPlayer = 1
LayerPlayerProjectile = 2
_ = 3
LayerEnemy = 5
LayerEnemyProjectile = 6
_ = 7
LayerScreenBounds = 9
)
type Circle struct {
Center Vector
Radius float64
Layer int
}
func circleOffset(c1 Circle, offset Vector) Circle {
return Circle{
Radius: c1.Radius,
Center: vectorAdd(c1.Center, offset),
Layer: c1.Layer,
}
}
func collides(c1, c2 Circle) bool {
dist := vectorDistance(c1.Center, c2.Center)
collides := dist <= c1.Radius+c2.Radius
seperation := absInt(c1.Layer - c2.Layer)
// Don't collide elements that are on the same layer, or an adjacent one
if collides && (seperation > 1) {
return collides
}
return false
}
func CheckCollisions() error {
for i := 0; i < len(Elements)-1; i++ {
for j := i + 1; j < len(Elements); j++ {
for _, c1 := range Elements[i].Collisions {
for _, c2 := range Elements[j].Collisions {
if collides(circleOffset(c1, Elements[i].Position), circleOffset(c2, Elements[j].Position)) && Elements[i].Active && Elements[j].Active {
if (Elements[i].parentElement != Elements[j]) && (Elements[j].parentElement != Elements[i]) {
err := Elements[i].collision(Elements[j])
if err != nil {
return err
}
err = Elements[j].collision(Elements[i])
if err != nil {
return err
}
}
}
}
}
}
}
return nil
}
+53
View File
@@ -0,0 +1,53 @@
package GoRetro
import (
"fmt"
"github.com/veandco/go-sdl2/sdl"
)
var TexList map[string]*sdl.Texture
func drawTexture(
tex *sdl.Texture,
position Vector,
rotation float64,
renderer *sdl.Renderer) error {
_, _, width, height, err := tex.Query()
if err != nil {
return fmt.Errorf("querying texture: %v", err)
}
// Convert coordinates to the top left of the sprite
position.X -= float64(width) / 2.0
position.Y -= float64(height) / 2.0
return renderer.CopyEx(
tex,
&sdl.Rect{X: 0, Y: 0, W: width, H: height},
&sdl.Rect{X: int32(position.X), Y: int32(position.Y), W: width, H: height},
rotation,
&sdl.Point{X: width / 2, Y: height / 2},
sdl.FLIP_NONE)
}
func loadTextureFromBMP(filename string, renderer *sdl.Renderer) (*sdl.Texture, error) {
if val, ok := TexList[filename]; ok {
return val, nil
}
img, err := sdl.LoadBMP(filename)
if err != nil {
return nil, fmt.Errorf("loading %v: %v", filename, err)
}
defer img.Free()
tex, err := renderer.CreateTextureFromSurface(img)
if err != nil {
return nil, fmt.Errorf("creating texture from %v: %v", filename, err)
}
TexList[filename] = tex
fmt.Printf("Caching %s\n", filename)
return tex, nil
}
+20
View File
@@ -0,0 +1,20 @@
package GoRetro
import "math"
type Vector struct {
X float64
Y float64
}
func vectorAdd(v1, v2 Vector) Vector {
return Vector{
X: v1.X + v2.X,
Y: v1.Y + v2.Y,
}
}
func vectorDistance(v1, v2 Vector) float64 {
return math.Sqrt(math.Pow(v2.X-v1.X, 2) +
math.Pow(v2.Y-v1.Y, 2))
}