diff --git a/assets/audiobank.go b/assets/audiobank.go new file mode 100644 index 0000000..5c6d736 --- /dev/null +++ b/assets/audiobank.go @@ -0,0 +1,57 @@ +package assets + +import ( + "bytes" + _ "embed" + "log" + + "github.com/hajimehoshi/ebiten/v2/audio/wav" +) + +type SndAssetName string + +const ( + MainLoop SndAssetName = "MainLoop" + Explode SndAssetName = "Explode" + + sampleRate = 44100 +) + +var ( + SoundBank map[SndAssetName]*wav.Stream + + //go:embed loop.wav + mainloop_snd []byte + //go:embed explode.wav + explode_snd []byte + + //go:embed explode.wav + Splode []byte + //go:embed hit.wav + TargetHit []byte + //go:embed herodeath.wav + HeroDeath []byte + //go:embed shot.wav + Shot []byte + //go:embed magic.wav + Magic []byte + //go:embed survive.wav + Survive []byte +) + +func LoadSounds() { + SoundBank = make(map[SndAssetName]*wav.Stream) + + SoundBank[MainLoop] = LoadSoundFatal(sampleRate, mainloop_snd) + SoundBank[Explode] = LoadSoundFatal(sampleRate, explode_snd) +} + +func LoadSoundFatal(rate int, obj []byte) *wav.Stream { + + stream, err := wav.DecodeWithSampleRate(rate, bytes.NewReader(obj)) + if err != nil { + log.Fatal("dead, jim") + } + + return stream +} diff --git a/assets/explode.wav b/assets/explode.wav new file mode 100644 index 0000000..8c729cb Binary files /dev/null and b/assets/explode.wav differ diff --git a/assets/herodeath.wav b/assets/herodeath.wav new file mode 100644 index 0000000..743dcb7 Binary files /dev/null and b/assets/herodeath.wav differ diff --git a/assets/hit.wav b/assets/hit.wav new file mode 100644 index 0000000..614b273 Binary files /dev/null and b/assets/hit.wav differ diff --git a/assets/imagebank.go b/assets/imagebank.go index 072b97e..2d34b88 100644 --- a/assets/imagebank.go +++ b/assets/imagebank.go @@ -10,27 +10,22 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) -type AssetName string +type ImgAssetName string const ( - FlyEyeNormal AssetName = "FlyEyeNormal" - FlyEyeDamaged AssetName = "FlyEyeDamaged" - FlyEyeDying AssetName = "FlyEyeDying" - FlyEyeShadow AssetName = "FlyEyeShadow" - HeroNormal AssetName = "HeroNormal" - HeroDying AssetName = "HeroDying" - TileSet AssetName = "TileSet" - Altar AssetName = "Altar" - Weapon AssetName = "Weapon" + FlyEyeNormal ImgAssetName = "FlyEyeNormal" + FlyEyeDamaged ImgAssetName = "FlyEyeDamaged" + FlyEyeDying ImgAssetName = "FlyEyeDying" + FlyEyeShadow ImgAssetName = "FlyEyeShadow" + HeroNormal ImgAssetName = "HeroNormal" + HeroDying ImgAssetName = "HeroDying" + TileSet ImgAssetName = "TileSet" + Altar ImgAssetName = "Altar" + Weapon ImgAssetName = "Weapon" ) var ( - ImageBank map[AssetName]*ebiten.Image - - flyeyeImage *ebiten.Image - flyeyeImage2 *ebiten.Image - flyeyeImage3 *ebiten.Image - shadow *ebiten.Image + ImageBank map[ImgAssetName]*ebiten.Image //go:embed fly-eye.png flyeye_img []byte @@ -53,7 +48,7 @@ var ( ) func LoadImages() { - ImageBank = make(map[AssetName]*ebiten.Image) + ImageBank = make(map[ImgAssetName]*ebiten.Image) ImageBank[FlyEyeNormal] = LoadImagesFatal(flyeye_img) ImageBank[FlyEyeDamaged] = LoadImagesFatal(flyeye_img2) diff --git a/assets/loop.wav b/assets/loop.wav new file mode 100644 index 0000000..944dd89 Binary files /dev/null and b/assets/loop.wav differ diff --git a/assets/magic.wav b/assets/magic.wav new file mode 100644 index 0000000..96de515 Binary files /dev/null and b/assets/magic.wav differ diff --git a/assets/shot.wav b/assets/shot.wav new file mode 100644 index 0000000..172cd55 Binary files /dev/null and b/assets/shot.wav differ diff --git a/assets/survive.wav b/assets/survive.wav new file mode 100644 index 0000000..a27bafd Binary files /dev/null and b/assets/survive.wav differ diff --git a/elements/mover.go b/elements/mover.go index c0bf275..fe03c9e 100644 --- a/elements/mover.go +++ b/elements/mover.go @@ -24,32 +24,34 @@ const ( type MoverAction uint type Mover struct { - Sprite *ebiten.Image - Maks *ebiten.Image - MaksDest *ebiten.Image - Angle float64 - Pos gamedata.Coordinates - Origin gamedata.Coordinates - Action MoverAction - cycles int - rotating bool - Toggled bool - Hit bool - Touched bool - dyingcount int + Sprite *ebiten.Image + Maks *ebiten.Image + MaksDest *ebiten.Image + Angle float64 + Pos gamedata.Coordinates + Origin gamedata.Coordinates + Action MoverAction + cycles int + rotating bool + Toggled bool + Hit bool + Touched bool + SplodeInitiated bool + dyingcount int } func NewMover() *Mover { m := &Mover{ - Sprite: ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT), - Maks: ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT), - MaksDest: ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT), - Action: MoverActionDefault, - cycles: 4, - Angle: 0, - rotating: false, - Toggled: false, - dyingcount: 0, + Sprite: ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT), + Maks: ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT), + MaksDest: ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT), + Action: MoverActionDefault, + cycles: 4, + Angle: 0, + rotating: false, + Toggled: false, + dyingcount: 0, + SplodeInitiated: false, } m.Maks.Fill(color.White) @@ -82,14 +84,8 @@ func (m *Mover) Draw() { switch m.Action { case MoverActionDefault: - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(14, 40) - m.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeShadow], op) m.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeNormal].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil) case MoverActionDamaged: - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(14, 40) - m.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeShadow], op) m.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil) case MoverActionDying: diff --git a/go.mod b/go.mod index d86b8a5..a5aa64c 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( require ( github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect github.com/ebitengine/hideconsole v1.0.0 // indirect + github.com/ebitengine/oto/v3 v3.3.1 // indirect github.com/ebitengine/purego v0.8.0 // indirect github.com/jezek/xgb v1.1.1 // indirect golang.org/x/sync v0.8.0 // indirect diff --git a/main.go b/main.go index aa0582d..549fd34 100644 --- a/main.go +++ b/main.go @@ -33,6 +33,7 @@ func main() { func loadScreens(m *screenmanager.Manager) { assets.LoadImages() + assets.LoadSounds() m.AddScene(screens.NewStartScreen()) m.AddScene(screens.NewGame()) m.ResetScenes() diff --git a/screenmanager/manager.go b/screenmanager/manager.go index d2086ef..30ace81 100644 --- a/screenmanager/manager.go +++ b/screenmanager/manager.go @@ -26,7 +26,7 @@ func NewManager() Manager { return Manager{ Info: gamedata.GameInfo{ Name: "survive", - Version: "0.12", + Version: "0.16", Dimensions: gamedata.Area{ Width: defaultWidth, Height: defaultHeight, diff --git a/screens/game.go b/screens/game.go index 927c543..5ee2750 100644 --- a/screens/game.go +++ b/screens/game.go @@ -11,9 +11,8 @@ import ( "mover/fonts" "mover/gamedata" - _ "embed" - "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/audio" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/text" "github.com/hajimehoshi/ebiten/v2/vector" @@ -22,6 +21,7 @@ import ( const ( MOVER_WIDTH = 48 MOVER_HEIGHT = 48 + sampleRate = 44100 ) type Game struct { @@ -32,16 +32,19 @@ type Game struct { heroCollisionMask *ebiten.Image heroCollisionCpy *ebiten.Image - dimensions gamedata.Area - Pos gamedata.Coordinates - Paused bool - initialized bool - gameover bool - reset bool - runtime float64 - hero *elements.Hero - projectiles map[int]*elements.Projectile - explosion *elements.Explosion + dimensions gamedata.Area + Pos gamedata.Coordinates + Paused bool + initialized bool + gameover bool + reset bool + musicInitialized bool + runtime float64 + hero *elements.Hero + projectiles map[int]*elements.Projectile + explosion *elements.Explosion + audioplayer *audio.Player + sfxplayer []*audio.Player score int counter int @@ -54,15 +57,39 @@ type Game struct { //pressedButtons map[ebiten.GamepadID][]string } +var ( + audioContext = audio.NewContext(sampleRate) +) + func NewGame() *Game { g := &Game{ - events: make(map[ScreenManagerEvent]func()), + events: make(map[ScreenManagerEvent]func()), + musicInitialized: false, } return g } func (g *Game) Initialize() { + if !g.musicInitialized { + //s, _ := wav.DecodeWithSampleRate(sampleRate) + //d, _ := wav.DecodeWithSampleRate(sampleRate, bytes.NewReader(loop_wav)) + + s := audio.NewInfiniteLoop(assets.SoundBank[assets.MainLoop], assets.SoundBank[assets.MainLoop].Length()) + + /* + + reader := bytes.NewReader(loop_wav) + + s := audio.NewInfiniteLoopF32(reader, reader.Length()) + */ + g.audioplayer, _ = audioContext.NewPlayer(s) + + g.audioplayer.Play() + + g.musicInitialized = true + } + origin := gamedata.Coordinates{X: 640 / 2, Y: 480 / 2} g.ConstructBackground() @@ -143,17 +170,6 @@ func (g *Game) Draw(screen *ebiten.Image) { g.runtime = float64(g.counter) / 60. } - s := fmt.Sprintf("%02.3f", g.runtime) - if !g.gameover { - text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White) - text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White) - } else { - if (g.counter/30)%2 == 0 { - text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White) - text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White) - } - } - op.GeoM.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2) //op.GeoM.Rotate(g.hero.Angle) op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y) @@ -175,7 +191,15 @@ func (g *Game) Draw(screen *ebiten.Image) { op.GeoM.Rotate(g.hero.Angle + math.Pi) op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y) screen.DrawImage(weaponImage, op) - }*/ + } + */ + + //draw shadows + for _, target := range g.targets { + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(target.Pos.X-10, target.Pos.Y+10) + screen.DrawImage(assets.ImageBank[assets.FlyEyeShadow], op) + } for _, target := range g.targets { target.Draw() @@ -201,6 +225,19 @@ func (g *Game) Draw(screen *ebiten.Image) { /*for _, gamepad ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisRightStickHorizontal), ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisRightStickVertical))*/ + + s := fmt.Sprintf("%02.3f", g.runtime) + if !g.gameover { + text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White) + text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White) + } else { + if (g.counter/30)%2 == 0 { + text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White) + text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White) + } + + text.Draw(screen, "PRESS START TO TRY AGAIN", fonts.SurviveFont.Arcade, 640/2-150, 480/2, color.White) + } } } @@ -237,6 +274,10 @@ func (g *Game) StepGame() { if !g.Paused { + if !g.audioplayer.IsPlaying() { + g.audioplayer.Play() + } + g.hero.Update() g.explosion.Update() @@ -255,6 +296,8 @@ func (g *Game) StepGame() { g.CleanupTargets() g.counter++ + } else { + g.audioplayer.Pause() } } @@ -351,6 +394,13 @@ func (g *Game) UpdateProjectiles() { target.SetHit() //target.SetOrigin(gamedata.Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480}) target.Hit = true + + //var err error + //player, err := audioContext.NewPlayer(assets.SoundBank[assets.Explode]) + + player := audioContext.NewPlayerFromBytes(assets.TargetHit) + player.Play() + break } } @@ -362,6 +412,12 @@ func (g *Game) UpdateProjectiles() { func (g *Game) UpdateTargets() { for _, target := range g.targets { + if target.Action == elements.MoverActionExploding && !target.SplodeInitiated { + player := audioContext.NewPlayerFromBytes(assets.Splode) + player.Play() + target.SplodeInitiated = true + } + if !target.Hit && g.hero.Action < elements.HeroActionDying { dx := g.hero.Pos.X - target.Pos.X dy := g.hero.Pos.Y - target.Pos.Y @@ -394,6 +450,9 @@ func (g *Game) UpdateTargets() { //fmt.Println("pixel death") g.hero.SetHit() g.gameover = true + + player := audioContext.NewPlayerFromBytes(assets.HeroDeath) + player.Play() break } } @@ -417,6 +476,9 @@ func (g *Game) AppendProjectiles() { if g.hero.Upgrade { g.projectiles[g.counter+1] = elements.NewProjectile(gamedata.Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle+math.Pi, 5.) } + + player := audioContext.NewPlayerFromBytes(assets.Shot) + player.Play() } } @@ -427,6 +489,8 @@ func (g *Game) HandleInput() { g.explosion.SetOrigin(g.hero.Pos) g.explosion.Reset() g.explosion.ToggleActivate() + player := audioContext.NewPlayerFromBytes(assets.Magic) + player.Play() } } @@ -453,6 +517,10 @@ func (g *Game) HandleInput() { inputangle := math.Atan2(yaxis, xaxis) g.hero.SetAngle(inputangle) } + + if inpututil.IsKeyJustPressed(ebiten.KeyQ) { + g.events[EventEndgame]() + } //} } diff --git a/screens/start.go b/screens/start.go index acdff18..6a6df84 100644 --- a/screens/start.go +++ b/screens/start.go @@ -3,6 +3,7 @@ package screens import ( "image/color" "math" + "mover/assets" "mover/fonts" "mover/gamedata" @@ -17,6 +18,8 @@ type StartScreen struct { target gamedata.Coordinates current gamedata.Coordinates targetreached bool + audioplayed bool + cycle int } func NewStartScreen() *StartScreen { @@ -25,6 +28,8 @@ func NewStartScreen() *StartScreen { target: gamedata.Coordinates{X: 640/2 - 150, Y: 480 / 2}, current: gamedata.Coordinates{X: 640/2 - 150, Y: -100}, targetreached: false, + cycle: 0, + audioplayed: false, } return s } @@ -36,13 +41,24 @@ func (s *StartScreen) Update() error { s.eHandler[EventCompleted]() } - s.current.X += (s.target.X - s.current.X) / 8 - s.current.Y += (s.target.Y - s.current.Y) / 8 + if !s.audioplayed { + player := audioContext.NewPlayerFromBytes(assets.Survive) + player.Play() + s.audioplayed = true + } + + if !s.targetreached { + s.current.X += (s.target.X - s.current.X) / 8 + s.current.Y += (s.target.Y - s.current.Y) / 8 + } else { + s.current.Y += 0.5 * math.Sin(float64(s.cycle)/(math.Pi*8)) + } if math.Abs(s.current.Y-s.target.Y) < 1 && !s.targetreached { s.targetreached = true } + s.cycle++ return nil }