Refactor to use better interfaces and event callbacks.

This commit is contained in:
2024-11-15 16:11:45 -05:00
parent 4ced75d66c
commit cbc4ba5eb3
16 changed files with 1009 additions and 14 deletions

View File

@@ -23,7 +23,8 @@ const (
TileSet ImgAssetName = "TileSet" TileSet ImgAssetName = "TileSet"
Altar ImgAssetName = "Altar" Altar ImgAssetName = "Altar"
Weapon ImgAssetName = "Weapon" Weapon ImgAssetName = "Weapon"
Worm ImgAssetName = "Worm" WormDamaged ImgAssetName = "WormDamaged"
Worm ImgAssetName = "WormDefault"
) )
var ( var (
@@ -51,6 +52,8 @@ var (
weapon_img []byte weapon_img []byte
//go:embed worm.png //go:embed worm.png
worm_img []byte worm_img []byte
//go:embed wormdefault.png
wormdefault_img []byte
) )
func LoadImages() { func LoadImages() {
@@ -66,7 +69,8 @@ func LoadImages() {
ImageBank[TileSet] = LoadImagesFatal(tileset_img) ImageBank[TileSet] = LoadImagesFatal(tileset_img)
ImageBank[Altar] = LoadImagesFatal(altar_img) ImageBank[Altar] = LoadImagesFatal(altar_img)
ImageBank[Weapon] = LoadImagesFatal(weapon_img) ImageBank[Weapon] = LoadImagesFatal(weapon_img)
ImageBank[Worm] = LoadImagesFatal(worm_img) ImageBank[WormDamaged] = LoadImagesFatal(worm_img)
ImageBank[Worm] = LoadImagesFatal(wormdefault_img)
} }

BIN
assets/worm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
assets/wormdefault.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -16,6 +16,8 @@ type Boss struct {
Spawned bool Spawned bool
Pos gamedata.Coordinates Pos gamedata.Coordinates
Right bool Right bool
Health int
Touched bool
damage bool damage bool
cycle int cycle int
Action MoverAction Action MoverAction
@@ -32,6 +34,8 @@ func NewBoss() *Boss {
hitcount: 0, hitcount: 0,
Maks: ebiten.NewImage(96, 96), Maks: ebiten.NewImage(96, 96),
MaskDest: ebiten.NewImage(96, 96), MaskDest: ebiten.NewImage(96, 96),
Health: 100,
Touched: false,
} }
b.Maks.Fill(color.White) b.Maks.Fill(color.White)
return b return b
@@ -45,6 +49,9 @@ func (b *Boss) Update() {
b.damageduration = 0 b.damageduration = 0
} }
} }
if b.Action == MoverActionDead {
b.Spawned = false
}
b.cycle++ b.cycle++
} }
@@ -71,15 +78,17 @@ func (b *Boss) Draw() {
switch b.Action { switch b.Action {
case MoverActionDefault: case MoverActionDefault:
b.Sprite.DrawImage(assets.ImageBank[assets.Worm].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
case MoverActionDamaged:
if (b.cycle/5)%2 == 0 && b.damage { if (b.cycle/5)%2 == 0 && b.damage {
b.MaskDest.DrawImage(assets.ImageBank[assets.Worm].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op) b.MaskDest.DrawImage(assets.ImageBank[assets.WormDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
op.GeoM.Reset() op.GeoM.Reset()
op.Blend = ebiten.BlendSourceAtop op.Blend = ebiten.BlendSourceAtop
b.MaskDest.DrawImage(b.Maks, op) b.MaskDest.DrawImage(b.Maks, op)
b.Sprite.DrawImage(b.MaskDest, nil) b.Sprite.DrawImage(b.MaskDest, nil)
} else { } else {
b.Sprite.DrawImage(assets.ImageBank[assets.Worm].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op) b.Sprite.DrawImage(assets.ImageBank[assets.WormDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
} }
case MoverActionExploding: case MoverActionExploding:
op.GeoM.Scale(2, 2) op.GeoM.Scale(2, 2)
@@ -95,7 +104,9 @@ func (b *Boss) Draw() {
func (b *Boss) SetHit() { func (b *Boss) SetHit() {
b.hitcount++ b.hitcount++
b.damage = true b.damage = true
if b.hitcount > 10 { b.Health--
if b.Health <= 0 {
b.Action = MoverActionExploding b.Action = MoverActionExploding
b.cycle = 0 b.cycle = 0
} }
@@ -107,4 +118,13 @@ func (b *Boss) Reset() {
b.damageduration = 0 b.damageduration = 0
b.Action = MoverActionDefault b.Action = MoverActionDefault
b.Spawned = false b.Spawned = false
b.Health = 100
}
func (b *Boss) ToggleColor() {
if b.Action == MoverActionDefault {
b.Action = MoverActionDamaged
} else if b.Action == MoverActionDamaged {
b.Action = MoverActionDefault
}
} }

23
elements/enemies.go Normal file
View File

@@ -0,0 +1,23 @@
package elements
import (
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type Enemies interface {
Update() error
Draw()
GetPosition() gamedata.Coordinates
SetPosition(gamedata.Coordinates)
SetTarget(gamedata.Coordinates)
GetSprite() *ebiten.Image
GetEnemyState() gamedata.EnemyState
SetHit()
SetToggle()
IsToggled() bool
SetTouched()
ClearTouched()
IsTouched() bool
}

153
elements/flyeye.go Normal file
View File

@@ -0,0 +1,153 @@
package elements
import (
"image"
"image/color"
"math"
"mover/assets"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type FlyEye struct {
Sprite *ebiten.Image
Maks *ebiten.Image
MaksDest *ebiten.Image
position gamedata.Coordinates
target gamedata.Coordinates
state gamedata.EnemyState
cycle int
dyingcount int
hit bool
touched bool
toggle bool
}
func NewFlyEye() *FlyEye {
f := &FlyEye{
Sprite: ebiten.NewImage(46, 46),
Maks: ebiten.NewImage(48, 48),
MaksDest: ebiten.NewImage(48, 48),
cycle: 0,
dyingcount: 0,
hit: false,
touched: false,
toggle: false,
}
f.Maks.Fill(color.White)
return f
}
func (f *FlyEye) Update() error {
//close loop on target
if f.state <= gamedata.EnemyStateHit {
dx := f.target.X - f.position.X
dy := f.target.Y - f.position.Y
if math.Abs(dx) > 3 || math.Abs(dy) > 3 {
angle := math.Atan2(dy, dx)
f.position.X += math.Cos(angle) * 3
f.position.Y += math.Sin(angle) * 3
}
}
if f.state == gamedata.EnemyStateDying {
f.dyingcount++
}
f.cycle++
return nil
}
func (f *FlyEye) Draw() {
f.Sprite.Clear()
f.MaksDest.Clear()
idx := (f.cycle / 8) % 4
y0 := 0
y1 := 48
x0 := 48 * idx
x1 := x0 + 48
switch f.state {
case gamedata.EnemyStateDefault:
if !f.toggle {
f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeNormal].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
} else {
f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
}
case gamedata.EnemyStateDying:
//after some condition, set to exploding
if (f.cycle/5)%2 == 0 {
f.MaksDest.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceAtop
f.MaksDest.DrawImage(f.Maks, op)
f.Sprite.DrawImage(f.MaksDest, nil)
} else {
f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
}
if f.dyingcount >= 31 {
f.cycle = 0
f.state = gamedata.EnemyStateExploding
}
case gamedata.EnemyStateExploding:
f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDying].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
if idx == 3 {
f.state = gamedata.EnemyStateDead
}
}
}
func (f *FlyEye) GetPosition() gamedata.Coordinates {
return f.position
}
func (f *FlyEye) SetTarget(p gamedata.Coordinates) {
f.target = p
}
func (f *FlyEye) GetSprite() *ebiten.Image {
return f.Sprite
}
func (f *FlyEye) SetHit() {
f.hit = true
f.state = gamedata.EnemyStateDying
f.cycle = 0
}
func (f *FlyEye) IsTouched() bool {
return f.touched
}
func (f *FlyEye) SetTouched() {
f.touched = true
}
func (f *FlyEye) ClearTouched() {
f.touched = false
}
func (f *FlyEye) SetToggle() {
f.toggle = !f.toggle
}
func (f *FlyEye) IsToggled() bool {
return f.toggle
}
func (f *FlyEye) GetEnemyState() gamedata.EnemyState {
return f.state
}
func (f *FlyEye) SetPosition(p gamedata.Coordinates) {
f.position = p
}

View File

@@ -114,7 +114,7 @@ func (m *Mover) Draw() {
} }
} }
func (m *Mover) Update() { func (m *Mover) Update() error {
/* /*
dx := 0. //40 * math.Cos(float64(m.cycles)/16) dx := 0. //40 * math.Cos(float64(m.cycles)/16)
dy := 0. //40 * math.Sin(float64(m.cycles)/16) dy := 0. //40 * math.Sin(float64(m.cycles)/16)
@@ -130,6 +130,7 @@ func (m *Mover) Update() {
m.dyingcount++ m.dyingcount++
} }
m.cycles++ m.cycles++
return nil
} }
func (m *Mover) SetHit() { func (m *Mover) SetHit() {

12
gamedata/enemystates.go Normal file
View File

@@ -0,0 +1,12 @@
package gamedata
type EnemyState int
const (
EnemyStateDefault = iota
EnemyStateHit
EnemyStateDying
EnemyStateExploding
EnemyStateDead
EnemyStateMax
)

11
gamedata/gameevents.go Normal file
View File

@@ -0,0 +1,11 @@
package gamedata
type GameEvent int
const (
GameEventPlayerDeath = iota
GameEventCharge
GameEventNewShot
GameEventTargetHit
GameEventExplosion
)

12
gamedata/gameinputs.go Normal file
View File

@@ -0,0 +1,12 @@
package gamedata
type GameInputs struct {
XAxis float64
YAxis float64
ShotAngle float64
Shot bool
Start bool
Charge bool
Quit bool
Reset bool
}

83
gameelement/background.go Normal file
View File

@@ -0,0 +1,83 @@
package gameelement
import (
"image"
"math/rand/v2"
"mover/assets"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type Background struct {
Sprite *ebiten.Image
initialized bool
}
func NewBackground(a gamedata.Area) *Background {
b := &Background{
Sprite: ebiten.NewImage(a.Width, a.Height),
initialized: false,
}
return b
}
func (b *Background) SetInputs(gamedata.GameInputs) {
}
func (b *Background) Update() error {
if !b.initialized {
b.Initialize()
} else {
}
return nil
}
func (b *Background) Draw(drawimg *ebiten.Image) {
//all the stuff before
op := &ebiten.DrawImageOptions{}
drawimg.DrawImage(b.Sprite, op)
}
func (b *Background) Initialize() {
b.ConstructBackground()
b.initialized = true
}
func (b *Background) ConstructBackground() {
BLOCK_SIZE := 32
for i := 0; i < b.Sprite.Bounds().Dx()/BLOCK_SIZE; i++ {
for j := 0; j < b.Sprite.Bounds().Dy()/BLOCK_SIZE; j++ {
//select random tile in x and y from tileset
idx_y := rand.IntN(256 / BLOCK_SIZE)
idx_x := rand.IntN(256 / BLOCK_SIZE)
x0 := BLOCK_SIZE * idx_x
y0 := BLOCK_SIZE * idx_y
x1 := x0 + BLOCK_SIZE
y1 := y0 + BLOCK_SIZE
//translate for grid element we're painting
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(i*BLOCK_SIZE), float64(j*BLOCK_SIZE))
b.Sprite.DrawImage(assets.ImageBank[assets.TileSet].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
}
}
ax := float64(rand.IntN(b.Sprite.Bounds().Dx()/BLOCK_SIZE) * BLOCK_SIZE)
ay := float64(rand.IntN(b.Sprite.Bounds().Dy()/BLOCK_SIZE) * BLOCK_SIZE)
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(ax, ay)
b.Sprite.DrawImage(assets.ImageBank[assets.Altar], op)
}
func (b *Background) RegisterEvents(e gamedata.GameEvent, f func()) {
}

403
gameelement/canvas.go Normal file
View File

@@ -0,0 +1,403 @@
package gameelement
import (
"fmt"
"image/color"
"math"
"math/rand/v2"
"mover/assets"
"mover/elements"
"mover/fonts"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/text"
"github.com/hajimehoshi/ebiten/v2/vector"
)
type Canvas struct {
Sprite *ebiten.Image
collisionMask *ebiten.Image
projectileMask *ebiten.Image
heroCollisionMask *ebiten.Image
heroCollisionCpy *ebiten.Image
eventmap map[gamedata.GameEvent]func()
initialized bool
lastInputs gamedata.GameInputs
runtime float64
counter int
score int
hero *elements.Hero
charge *elements.Explosion
enemies []elements.Enemies
projectiles []*elements.Projectile
gameover bool
}
func NewCanvas(a gamedata.Area) *Canvas {
c := &Canvas{
Sprite: ebiten.NewImage(a.Width, a.Height),
projectileMask: ebiten.NewImage(a.Width, a.Height),
collisionMask: ebiten.NewImage(a.Width, a.Height),
heroCollisionMask: ebiten.NewImage(46, 46),
heroCollisionCpy: ebiten.NewImage(46, 46),
hero: elements.NewHero(),
charge: elements.NewExplosion(),
initialized: false,
gameover: false,
score: 0,
runtime: 0.,
}
c.eventmap = make(map[gamedata.GameEvent]func())
return c
}
func (c *Canvas) SetInputs(gi gamedata.GameInputs) {
c.lastInputs = gi
}
func (c *Canvas) Update() error {
if !c.initialized {
c.Initialize()
} else {
//update positions()
//hero first
c.UpdateHero()
c.UpdateProjectiles()
c.UpdateCharge()
c.UpdateEnemies()
c.CleanupTargets()
}
c.counter++
return nil
}
func (c *Canvas) Draw(drawimg *ebiten.Image) {
c.Sprite.Clear()
c.projectileMask.Clear()
//vector.DrawFilledCircle(c.Sprite, float32(c.hero.Pos.X), float32(c.hero.Pos.Y), 100, color.White, true)
c.hero.Draw()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(c.hero.Pos.X-48/2, c.hero.Pos.Y-48/2)
c.Sprite.DrawImage(c.hero.Sprite, op)
if !c.gameover {
op.GeoM.Reset()
op.GeoM.Translate(0, -16)
op.GeoM.Rotate(c.lastInputs.ShotAngle)
op.GeoM.Translate(c.hero.Pos.X, c.hero.Pos.Y)
c.Sprite.DrawImage(assets.ImageBank[assets.Weapon], op)
}
for _, e := range c.enemies {
e.Draw()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(e.GetPosition().X-46/2, e.GetPosition().Y-46/2)
c.Sprite.DrawImage(e.GetSprite(), op)
}
for _, p := range c.projectiles {
//drawimg.DrawImage()
vector.DrawFilledCircle(c.projectileMask, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true)
}
c.Sprite.DrawImage(c.projectileMask, nil)
vector.StrokeCircle(c.Sprite, float32(c.charge.Origin.X), float32(c.charge.Origin.Y), float32(c.charge.Radius), 3, color.White, true)
if !c.gameover {
c.runtime = float64(c.counter) / 60.
}
s := fmt.Sprintf("%02.3f", c.runtime)
if !c.gameover {
text.Draw(c.Sprite, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White)
text.Draw(c.Sprite, fmt.Sprintf("SCORE: %d", c.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White)
} else {
if (c.counter/30)%2 == 0 {
text.Draw(c.Sprite, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White)
text.Draw(c.Sprite, fmt.Sprintf("SCORE: %d", c.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White)
}
text.Draw(c.Sprite, "PRESS START TO TRY AGAIN", fonts.SurviveFont.Arcade, 640/2-150, 480/2, color.White)
}
//op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
drawimg.DrawImage(c.Sprite, op)
}
func (c *Canvas) Initialize() {
c.InitializeHero()
c.enemies = c.enemies[:0]
c.gameover = false
c.initialized = true
c.score = 0
c.counter = 0
c.runtime = 0.
//temporary
c.hero.Action = elements.HeroActionDefault
}
func (c *Canvas) UpdateHero() {
c.hero.Update()
if !c.gameover {
c.UpdateHeroPosition()
c.ComputeHeroCollisions()
c.AddProjectiles()
}
}
func (c *Canvas) UpdateHeroPosition() {
if c.lastInputs.XAxis >= 0.15 || c.lastInputs.XAxis <= -0.15 {
c.hero.Left = c.lastInputs.XAxis < 0
c.hero.Pos.X += c.lastInputs.XAxis * 5
}
if c.lastInputs.YAxis >= 0.15 || c.lastInputs.YAxis <= -0.15 {
c.hero.Pos.Y += c.lastInputs.YAxis * 5
}
}
func (c *Canvas) ComputeHeroCollisions() {
for _, e := range c.enemies {
//compute collision with hero
if c.hero.Pos.X >= e.GetPosition().X-46/2 && c.hero.Pos.X <= e.GetPosition().X+46/2 &&
c.hero.Pos.Y >= e.GetPosition().Y-46/2 && c.hero.Pos.Y <= e.GetPosition().Y+46/2 &&
e.GetEnemyState() < gamedata.EnemyStateDying {
// target.Action < elements.MoverActionDying && g.hero.Action < elements.HeroActionDying {
c.heroCollisionMask.Clear()
c.heroCollisionMask.DrawImage(c.hero.Sprite, nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceIn
op.GeoM.Translate((c.hero.Pos.X-e.GetPosition().X)-float64(e.GetSprite().Bounds().Dx())/2, (c.hero.Pos.Y-e.GetPosition().Y)-float64(e.GetSprite().Bounds().Dy())/2)
c.heroCollisionMask.DrawImage(e.GetSprite(), op)
if c.HasCollided(c.heroCollisionMask, 46*46*4) {
c.hero.SetHit()
c.gameover = true
if c.eventmap[gamedata.GameEventPlayerDeath] != nil {
c.eventmap[gamedata.GameEventPlayerDeath]()
}
}
}
}
}
func (c *Canvas) AddProjectiles() {
//add new projectiles
if c.lastInputs.Shot && c.counter%14 == 0 {
loc := gamedata.Coordinates{
X: c.hero.Pos.X,
Y: c.hero.Pos.Y,
}
angle := c.lastInputs.ShotAngle
velocity := 5.
c.projectiles = append(c.projectiles, elements.NewProjectile(loc, angle, velocity))
if c.hero.Upgrade {
c.projectiles = append(c.projectiles, elements.NewProjectile(loc, angle+math.Pi, velocity))
}
if c.eventmap[gamedata.GameEventNewShot] != nil {
c.eventmap[gamedata.GameEventNewShot]()
}
}
}
func (c *Canvas) InitializeHero() {
//recenter the hero
pos := gamedata.Coordinates{
X: float64(c.Sprite.Bounds().Dx() / 2),
Y: float64(c.Sprite.Bounds().Dy() / 2),
}
c.hero.SetOrigin(pos)
}
func (c *Canvas) UpdateProjectiles() {
i := 0
for _, p := range c.projectiles {
p.Update()
projectilevalid := true
if p.Pos.X < -640/2 || p.Pos.X > 1.5*640 || p.Pos.Y < -480/2 || p.Pos.Y > 1.5*480 {
projectilevalid = false
}
for _, e := range c.enemies {
if p.Pos.X >= e.GetPosition().X-48/2 && p.Pos.X <= e.GetPosition().X+48/2 &&
p.Pos.Y >= e.GetPosition().Y-48/2 && p.Pos.Y <= e.GetPosition().Y+48/2 &&
e.IsToggled() && e.GetEnemyState() < gamedata.EnemyStateDying {
c.collisionMask.Clear()
c.collisionMask.DrawImage(c.projectileMask, nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceIn
op.GeoM.Translate(e.GetPosition().X-float64(e.GetSprite().Bounds().Dx())/2, e.GetPosition().Y-float64(e.GetSprite().Bounds().Dy())/2)
c.collisionMask.DrawImage(e.GetSprite(), op)
if c.HasCollided(c.collisionMask, 640*480*4) {
//fmt.Println("pixel collision")
//delete(g.projectiles, k)
projectilevalid = false
//target.ToggleColor()
e.SetHit()
//target.SetOrigin(gamedata.Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480})
//target.Hit = true
/*player := audioContext.NewPlayerFromBytes(assets.TargetHit)
player.Play()*/
if c.eventmap[gamedata.GameEventTargetHit] != nil {
c.eventmap[gamedata.GameEventTargetHit]()
}
}
}
}
if projectilevalid {
c.projectiles[i] = p
i++
}
}
for j := i; j < len(c.projectiles); j++ {
c.projectiles[j] = nil
}
c.projectiles = c.projectiles[:i]
}
func (c *Canvas) UpdateCharge() {
if c.lastInputs.Charge && !c.charge.Active && !c.gameover {
c.charge.SetOrigin(c.hero.Pos)
c.charge.Reset()
c.charge.ToggleActivate()
if c.eventmap[gamedata.GameEventCharge] != nil {
c.eventmap[gamedata.GameEventCharge]()
}
}
c.charge.Update()
if c.charge.Active {
if c.charge.Radius > math.Sqrt(640*640+480*480) {
c.charge.ToggleActivate()
c.charge.Reset()
c.ResetTargetTouches()
}
for _, e := range c.enemies {
dx := e.GetPosition().X - c.hero.Pos.X
dy := e.GetPosition().Y - c.hero.Pos.Y
r := math.Sqrt(dx*dx + dy*dy)
if r >= c.charge.Radius-5 && r <= c.charge.Radius+5 &&
!e.IsTouched() && e.GetEnemyState() <= gamedata.EnemyStateHit {
e.SetToggle()
e.SetTouched()
}
}
}
}
func (c *Canvas) ResetTargetTouches() {
for _, e := range c.enemies {
e.ClearTouched()
}
}
func (c *Canvas) UpdateEnemies() {
//update existing enemies
for _, e := range c.enemies {
if !c.gameover {
e.SetTarget(c.hero.Pos)
} else {
e.SetTarget(e.GetPosition())
}
e.Update()
}
if !c.gameover {
//spawn new enemies
f := 40000 / (c.counter + 1)
if c.counter%f == 0 {
newenemy := elements.NewFlyEye()
x0 := rand.Float64() * 640
y0 := rand.Float64() * 480
quadrant := rand.IntN(3)
switch quadrant {
case 0:
newenemy.SetPosition(gamedata.Coordinates{X: x0, Y: -48})
case 1:
newenemy.SetPosition(gamedata.Coordinates{X: x0, Y: 480 + 48})
case 2:
newenemy.SetPosition(gamedata.Coordinates{X: -48, Y: y0})
case 3:
newenemy.SetPosition(gamedata.Coordinates{X: 640 + x0, Y: y0})
}
newenemy.SetTarget(c.hero.Pos)
c.enemies = append(c.enemies, newenemy)
}
}
}
func (c *Canvas) HasCollided(mask *ebiten.Image, size int) bool {
var result bool = false
var pixels []byte = make([]byte, size)
mask.ReadPixels(pixels)
for i := 0; i < len(pixels); i = i + 4 {
if pixels[i+3] != 0 {
result = true
break
}
}
return result
}
func (c *Canvas) RegisterEvents(e gamedata.GameEvent, f func()) {
c.eventmap[e] = f
}
func (c *Canvas) CleanupTargets() {
// remove dead targets by iterating over all targets
i := 0
for _, e := range c.enemies {
//moving valid targets to the front of the slice
if e.GetEnemyState() < elements.MoverActionDead {
c.enemies[i] = e
i++
}
}
//then culling the last elements of the slice, and conveniently we can update
//our base score with the number of elements removed (bonuses calculated elsewhere)
if len(c.enemies)-i > 0 {
c.score += len(c.enemies) - i
}
for j := i; j < len(c.enemies); j++ {
c.enemies[j] = nil
}
c.enemies = c.enemies[:i]
}

View File

@@ -0,0 +1,15 @@
package gameelement
import (
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type GameElement interface {
SetInputs(gamedata.GameInputs)
Update() error
Draw(drawimg *ebiten.Image)
Initialize()
RegisterEvents(e gamedata.GameEvent, f func())
}

View File

@@ -35,6 +35,7 @@ func loadScreens(m *screenmanager.Manager) {
assets.LoadImages() assets.LoadImages()
assets.LoadSounds() assets.LoadSounds()
m.AddScene(screens.NewStartScreen()) m.AddScene(screens.NewStartScreen())
m.AddScene(screens.NewGame()) //m.AddScene(screens.NewGame())
m.AddScene(screens.NewPrimary())
m.ResetScenes() m.ResetScenes()
} }

View File

@@ -62,6 +62,7 @@ func NewGame() *Game {
musicInitialized: false, musicInitialized: false,
boss: elements.NewBoss(), boss: elements.NewBoss(),
} }
return g return g
} }
@@ -195,6 +196,14 @@ func (g *Game) Draw(screen *ebiten.Image) {
op.GeoM.Translate(-MOVER_WIDTH, -MOVER_HEIGHT) op.GeoM.Translate(-MOVER_WIDTH, -MOVER_HEIGHT)
op.GeoM.Translate(g.boss.Pos.X, g.boss.Pos.Y) op.GeoM.Translate(g.boss.Pos.X, g.boss.Pos.Y)
screen.DrawImage(g.boss.Sprite, op) screen.DrawImage(g.boss.Sprite, op)
//text.Draw(screen, fmt.Sprintf("%d", g.boss.Health), fonts.SurviveFont.Arcade, 100, 50, color.White)
//boss health bar
x0 := g.boss.Pos.X - 96
y0 := g.boss.Pos.Y - 60
vector.DrawFilledRect(screen, float32(x0), float32(y0), 204, 12, color.Black, true)
vector.DrawFilledRect(screen, float32(x0+2), float32(y0+2), float32(g.boss.Health)*2, 8, color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}, true)
} }
g.projectileMask.Clear() g.projectileMask.Clear()
@@ -280,14 +289,15 @@ func (g *Game) StepGame() {
//append new projectiles //append new projectiles
g.AppendProjectiles() g.AppendProjectiles()
//add new target with increasing frequency
g.SpawnEnemies()
//handle pulsewave updates //handle pulsewave updates
g.HandlePulseWaveUpdate() g.HandlePulseWaveUpdate()
if !g.boss.Spawned && g.counter > 600 { if !g.boss.Spawned {
g.SpawnBoss() //add new target with increasing frequency
g.SpawnEnemies()
if g.counter > 2000 {
g.SpawnBoss()
}
} }
} }
@@ -346,6 +356,20 @@ func (g *Game) HandlePulseWaveUpdate() {
//target.SetHit() //target.SetHit()
} }
} }
//check for boss
if g.boss.Spawned {
dx := g.boss.Pos.X - g.hero.Pos.X
dy := g.boss.Pos.Y - g.hero.Pos.Y
r := math.Sqrt(dx*dx + dy*dy)
if r >= g.explosion.Radius-40 && r <= g.explosion.Radius+40 &&
g.boss.Action <= elements.MoverActionDamaged && !g.boss.Touched {
g.boss.ToggleColor()
g.boss.Touched = true
//target.SetHit()
}
}
} }
} }
@@ -393,10 +417,10 @@ func (g *Game) UpdateProjectiles() {
} }
} }
//boss check first, boundary check //boss check: first, boundary check
if p.Pos.X >= g.boss.Pos.X-MOVER_WIDTH && p.Pos.X <= g.boss.Pos.X+MOVER_WIDTH && if p.Pos.X >= g.boss.Pos.X-MOVER_WIDTH && p.Pos.X <= g.boss.Pos.X+MOVER_WIDTH &&
p.Pos.Y >= g.boss.Pos.Y-MOVER_HEIGHT && p.Pos.Y <= g.boss.Pos.Y+MOVER_HEIGHT && p.Pos.Y >= g.boss.Pos.Y-MOVER_HEIGHT && p.Pos.Y <= g.boss.Pos.Y+MOVER_HEIGHT &&
g.boss.Action < elements.MoverActionDying { g.boss.Action == elements.MoverActionDamaged {
//fmt.Println("potential collision") //fmt.Println("potential collision")
//the following computes total collisions in the image using a projectile mask that is a duplicate of what is on screen //the following computes total collisions in the image using a projectile mask that is a duplicate of what is on screen
@@ -476,6 +500,7 @@ func (g *Game) ResetTargetTouches() {
for _, t := range g.targets { for _, t := range g.targets {
t.Touched = false t.Touched = false
} }
g.boss.Touched = false
} }
func (g *Game) AppendProjectiles() { func (g *Game) AppendProjectiles() {
@@ -636,6 +661,7 @@ func (g *Game) UpdateBoss() {
g.boss.Update() g.boss.Update()
if g.boss.Action == elements.MoverActionExploding && !g.boss.SplodeInitiated { if g.boss.Action == elements.MoverActionExploding && !g.boss.SplodeInitiated {
g.score += 10
player := audioContext.NewPlayerFromBytes(assets.Splode) player := audioContext.NewPlayerFromBytes(assets.Splode)
player.Play() player.Play()
g.boss.SplodeInitiated = true g.boss.SplodeInitiated = true
@@ -697,3 +723,7 @@ func (g *Game) HasCollided(mask *ebiten.Image, size int) bool {
} }
return result return result
} }
func (g *Game) SetInputs(gamedata.GameInputs) {
}

227
screens/primary.go Normal file
View File

@@ -0,0 +1,227 @@
package screens
import (
"math"
"mover/assets"
"mover/gamedata"
"mover/gameelement"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type Primary struct {
events map[ScreenManagerEvent]func()
dimensions gamedata.Area
elements []gameelement.GameElement
gameevents map[gamedata.GameEvent]bool
paused bool
gameover bool
musicInitialized bool
audioplayer *audio.Player
}
func NewPrimary() *Primary {
p := &Primary{
events: make(map[ScreenManagerEvent]func()),
paused: false,
gameover: false,
musicInitialized: false,
}
p.gameevents = make(map[gamedata.GameEvent]bool)
p.elements = append(p.elements, gameelement.NewBackground(gamedata.Area{Width: 640, Height: 480}))
canvas := gameelement.NewCanvas(gamedata.Area{Width: 640, Height: 480})
canvas.RegisterEvents(gamedata.GameEventPlayerDeath, p.EventHandlerPlayerDeath)
canvas.RegisterEvents(gamedata.GameEventCharge, p.EventHandlerCharge)
canvas.RegisterEvents(gamedata.GameEventNewShot, p.EventHandlerNewShot)
canvas.RegisterEvents(gamedata.GameEventTargetHit, p.EventHandlerTargetHit)
canvas.RegisterEvents(gamedata.GameEventExplosion, p.EventHandlerExplosion)
p.elements = append(p.elements, canvas)
return p
}
func (p *Primary) Update() error {
if !p.musicInitialized {
s := audio.NewInfiniteLoop(assets.SoundBank[assets.MainLoop], assets.SoundBank[assets.MainLoop].Length())
p.audioplayer, _ = audioContext.NewPlayer(s)
p.audioplayer.Play()
p.musicInitialized = true
}
//collect all inputs
inputs := p.CollectInputs()
if inputs.Quit {
p.events[EventEndgame]()
}
if inputs.Reset {
p.Reset()
}
p.ProcessEventAudio()
if inputs.Start {
if p.gameover {
p.Reset()
} else {
p.TogglePause()
}
}
//primary game loop, for each element pass along the inputs
//and process its update logic
if !p.paused {
for _, ge := range p.elements {
ge.SetInputs(inputs)
ge.Update()
}
}
return nil
}
func (p *Primary) Draw(screen *ebiten.Image) {
//here we simply call each game elements draw function
//as a layer on top of each other
for _, ge := range p.elements {
ge.Draw(screen)
}
}
func (p *Primary) SetEventHandler(e ScreenManagerEvent, f func()) {
p.events[e] = f
}
func (p *Primary) SetDimensions(a gamedata.Area) {
p.dimensions = a
}
func (p *Primary) CollectInputs() gamedata.GameInputs {
if inpututil.IsKeyJustPressed(ebiten.KeyQ) {
p.events[EventEndgame]()
}
gi := gamedata.GameInputs{}
//axes
inpx := ebiten.GamepadAxisValue(0, 0)
inpy := ebiten.GamepadAxisValue(0, 1)
//handle wasd input
if ebiten.IsKeyPressed(ebiten.KeyD) {
inpx = 1
}
if ebiten.IsKeyPressed(ebiten.KeyA) {
inpx = -1
}
if ebiten.IsKeyPressed(ebiten.KeyS) {
inpy = 1
}
if ebiten.IsKeyPressed(ebiten.KeyW) {
inpy = -1
}
gi.XAxis = inpx
gi.YAxis = inpy
xaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal)
yaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickVertical)
if yaxis <= 0.09 && yaxis >= -0.09 {
yaxis = 0
}
if xaxis <= 0.09 && xaxis >= -0.09 {
xaxis = 0
}
gi.ShotAngle = math.Atan2(yaxis, xaxis)
gi.Charge = inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonRightStick)
gi.Start = inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonCenterRight)
gi.Shot = ebiten.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonFrontBottomRight)
gi.Quit = inpututil.IsKeyJustPressed(ebiten.KeyQ)
gi.Reset = inpututil.IsKeyJustPressed(ebiten.KeyR)
return gi
}
func (p *Primary) TogglePause() {
p.paused = !p.paused
var player *audio.Player
if p.paused {
player = audioContext.NewPlayerFromBytes(assets.PauseIn)
p.audioplayer.Pause()
} else {
player = audioContext.NewPlayerFromBytes(assets.PauseOut)
p.audioplayer.Play()
}
player.Play()
}
func (p *Primary) Reset() {
p.paused = false
p.gameover = false
for _, ge := range p.elements {
ge.Initialize()
}
}
func (p *Primary) ProcessEventAudio() {
for event, occurred := range p.gameevents {
if occurred {
p.PlayAudio(event)
p.gameevents[event] = false
}
}
}
func (p *Primary) PlayAudio(e gamedata.GameEvent) {
switch e {
case gamedata.GameEventPlayerDeath:
player := audioContext.NewPlayerFromBytes(assets.HeroDeath)
player.Play()
case gamedata.GameEventCharge:
player := audioContext.NewPlayerFromBytes(assets.Magic)
player.Play()
case gamedata.GameEventNewShot:
player := audioContext.NewPlayerFromBytes(assets.Shot)
player.Play()
case gamedata.GameEventTargetHit:
player := audioContext.NewPlayerFromBytes(assets.TargetHit)
player.Play()
case gamedata.GameEventExplosion:
player := audioContext.NewPlayerFromBytes(assets.Splode)
player.Play()
}
}
func (p *Primary) EventHandlerPlayerDeath() {
p.gameevents[gamedata.GameEventPlayerDeath] = true
p.gameover = true
}
func (p *Primary) EventHandlerCharge() {
p.gameevents[gamedata.GameEventCharge] = true
}
func (p *Primary) EventHandlerNewShot() {
p.gameevents[gamedata.GameEventNewShot] = true
}
func (p *Primary) EventHandlerTargetHit() {
p.gameevents[gamedata.GameEventTargetHit] = true
}
func (p *Primary) EventHandlerExplosion() {
p.gameevents[gamedata.GameEventExplosion] = true
}