Added music & game sounds.

User can now use mouse to select controls.
This commit is contained in:
Sander Schobers 2021-08-12 22:28:31 +02:00
parent cbd08cdc12
commit e3527eb580
8 changed files with 395 additions and 38 deletions

View File

@ -1,12 +1,14 @@
- [X] Increase difficulty. - [X] Increase difficulty.
- [ ] Add music & sounds. - [X] Add music & sounds.
- [X] Keep score/difficulty level (resume & restart). - [X] Keep score/difficulty level (resume & restart).
- [X] ~~Explain controls on info page~~ add settings for controls. - [X] ~~Explain controls on info page~~ add settings for controls.
- [X] Fix usage of go/embed (and remove rice again). - [X] Fix usage of go/embed (and remove rice again).
- [X] Add monster animations (~~jumping on tile &~~ towards new tile). - [X] Add monster animations (~~jumping on tile &~~ towards new tile).
- [ ] Scale icons (heart & star on right side) when playing. - [X] ~~Scale icons (heart & star on right side) when playing.~~
- [ ] Change layout when playing in portrait mode. - [ ] Change layout when playing in portrait mode.
- [X] Fix wobble animation. - [X] Fix wobble animation.
- [ ] Add more unit tests? - [ ] Add more unit tests?
- [ ] Fix z-fighting of monsters. - [ ] Fix z-fighting of monsters.
- [ ] Add exploding animation of monsters. - [ ] Add exploding animation of monsters.
- [ ] Add audio settings (music & sound volume).
- [ ] Hearts must be saved as well for resume.

View File

@ -61,6 +61,21 @@ func (a *app) Init(ctx ui.Context) error {
}) })
a.context.ShowMainMenu(ctx) a.context.ShowMainMenu(ctx)
err := a.context.Audio.LoadSample(
"level_completed.mp3",
"level_game_over.mp3",
"level_new_high_score.mp3",
"menu_interaction.mp3",
"monster_jump.mp3",
"player_collect_heart.mp3",
"player_collect_star.mp3",
"player_hurt.mp3",
"player_move.mp3",
)
if err != nil {
return err
}
return nil return nil
} }

View File

@ -1,6 +1,9 @@
package main package main
import ( import (
"fmt"
"math/rand"
"opslag.de/schobers/tins2021" "opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui" "opslag.de/schobers/zntg/ui"
) )
@ -12,10 +15,14 @@ type appContext struct {
Score *tins2021.ScoreState Score *tins2021.ScoreState
Debug bool Debug bool
StarTexture tins2021.AnimatedTexture StarTexture tins2021.AnimatedTexture
HeartTexture tins2021.AnimatedTexture HeartTexture tins2021.AnimatedTexture
MonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture MonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture
Audio *AudioPlayer
MenuMusic *Music
GameMusic *Music
GameMusicSong int
} }
func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.ScoreState, setView func(ui.Control)) *appContext { func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.ScoreState, setView func(ui.Control)) *appContext {
@ -23,6 +30,7 @@ func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.
app := &appContext{ app := &appContext{
setView: setView, setView: setView,
Settings: settings, Settings: settings,
Audio: NewAudioPlayer(ctx.Resources(), "resources/sounds/"),
Score: score, Score: score,
StarTexture: newAnimatedTexture(ctx, "star", defaultAnimationFrames, textures.Star), StarTexture: newAnimatedTexture(ctx, "star", defaultAnimationFrames, textures.Star),
HeartTexture: newAnimatedTexture(ctx, "heart", defaultAnimationFrames, textures.Heart), HeartTexture: newAnimatedTexture(ctx, "heart", defaultAnimationFrames, textures.Heart),
@ -38,12 +46,16 @@ func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.
const numberOfStars = 5 const numberOfStars = 5
func (app *appContext) MenuInteraction() {
app.Audio.PlaySample("menu_interaction.mp3")
}
func (app *appContext) Play(ctx ui.Context) { func (app *appContext) Play(ctx ui.Context) {
level := tins2021.NewLevel() level := tins2021.NewLevel()
level.Randomize(0, numberOfStars) level.Randomize(0, numberOfStars)
app.Score.Current = tins2021.Score{} app.Score.Current = tins2021.Score{}
app.show(newLevelControl(app, ctx, level)) app.show(ctx, newLevelControl(app, ctx, level))
} }
func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) { func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) {
@ -64,29 +76,82 @@ func (app *appContext) PlayResume(ctx ui.Context) {
level.Score = app.Score.Current.Score level.Score = app.Score.Current.Score
level.Randomize(app.Score.Current.Difficulty, numberOfStars) level.Randomize(app.Score.Current.Difficulty, numberOfStars)
app.show(newLevelControl(app, ctx, level)) app.show(ctx, newLevelControl(app, ctx, level))
} }
func (app *appContext) show(control ui.Control) { func (app *appContext) show(ctx ui.Context, control ui.Control) {
app.setView(control) app.setView(control)
if _, ok := control.(*levelController); ok {
app.switchToPlayMusic(ctx)
} else {
app.switchToMenuMusic(ctx)
}
} }
func (app *appContext) ShowCredits(ctx ui.Context) { func (app *appContext) ShowCredits(ctx ui.Context) {
app.setView(newCredits(app, ctx)) app.show(ctx, newCredits(app, ctx))
} }
func (app *appContext) ShowSettings(ctx ui.Context) { func (app *appContext) ShowSettings(ctx ui.Context) {
app.setView(newSettings(app, ctx)) app.show(ctx, newSettings(app, ctx))
} }
func (app *appContext) ShowHighscores(ctx ui.Context) { func (app *appContext) ShowHighscores(ctx ui.Context) {
app.setView(newHighscores(app, ctx)) app.show(ctx, newHighscores(app, ctx))
} }
func (app *appContext) ShowInfo(ctx ui.Context) { func (app *appContext) ShowInfo(ctx ui.Context) {
app.setView(newInfo(app, ctx)) app.show(ctx, newInfo(app, ctx))
} }
func (app *appContext) ShowMainMenu(ctx ui.Context) { func (app *appContext) ShowMainMenu(ctx ui.Context) {
app.show(newMainMenu(app, ctx)) app.show(ctx, newMainMenu(app, ctx))
}
func (app *appContext) playNextGameMusic(ctx ui.Context) {
if app.GameMusic != nil {
return
}
const songs = 4
pick := func() int {
for {
s := rand.Intn(songs) + 1
if s == app.GameMusicSong {
continue
}
app.GameMusicSong = s
return s
}
}
song := fmt.Sprintf("song_game_%d.mp3", pick())
app.GameMusic, _ = app.Audio.PlayMusic(song, func(m *Music) {
m.OnFinished = func() {
if app.GameMusic == nil {
return
}
app.GameMusic = nil
app.playNextGameMusic(ctx)
}
})
}
func (app *appContext) switchToPlayMusic(ctx ui.Context) {
app.playNextGameMusic(ctx)
menuMusic := app.MenuMusic
app.MenuMusic = nil
if menuMusic != nil {
menuMusic.Stop()
}
}
func (app *appContext) switchToMenuMusic(ctx ui.Context) {
if app.MenuMusic != nil {
return
}
app.MenuMusic, _ = app.Audio.PlayMusic("song_menu.mp3", func(m *Music) {})
gameMusic := app.GameMusic
app.GameMusic = nil
if gameMusic != nil {
gameMusic.Stop()
}
} }

181
cmd/tins2021/audio.go Normal file
View File

@ -0,0 +1,181 @@
package main
import (
"fmt"
"io"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/effects"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
"opslag.de/schobers/zntg/ui"
)
type AudioPlayer struct {
resources ui.Resources
prefix string
SampleRate beep.SampleRate
Samples map[string]Sample
SampleVolume float64
MusicVolume float64
}
func NewAudioPlayer(resources ui.Resources, prefix string) *AudioPlayer {
var rate = beep.SampleRate(48000)
speaker.Init(rate, rate.N(time.Second/20))
return &AudioPlayer{
resources: resources,
prefix: prefix,
SampleRate: rate,
Samples: map[string]Sample{},
SampleVolume: 1,
MusicVolume: 1,
}
}
func (p *AudioPlayer) LoadSample(name ...string) error {
for _, name := range name {
if _, err := p.openSample(name); err != nil {
return err
}
}
return nil
}
func (p *AudioPlayer) openSample(name string) (Sample, error) {
sample, ok := p.Samples[name]
if ok {
return sample, nil
}
stream, format, err := p.openResource(name)
if err != nil {
return Sample{}, err
}
defer stream.Close()
buffer := beep.NewBuffer(format)
buffer.Append(stream)
sample = Sample{Buffer: buffer, SampleRate: format.SampleRate}
p.Samples[name] = sample
return sample, nil
}
func (p *AudioPlayer) PlaySample(name string) error {
sample, err := p.openSample(name)
if err != nil {
return err
}
speaker.Play(p.resample(sample.Stream(), sample.SampleRate))
return nil
}
func (p *AudioPlayer) resample(stream beep.Streamer, sampleRate beep.SampleRate) beep.Streamer {
return Resample(stream, p.SampleRate, sampleRate)
}
func (p *AudioPlayer) PlayMusic(name string, init func(*Music)) (*Music, error) {
stream, format, err := p.openResource(name)
if err != nil {
return nil, err
}
closer := &streamCloser{stream}
music := &Music{
Name: name,
AutoRepeat: false,
Stream: closer,
Volume: &effects.Volume{
Streamer: p.resample(closer, format.SampleRate),
Base: 2,
Volume: 1,
Silent: false,
},
}
if init != nil {
init(music)
}
speaker.Play(beep.Seq(music.Volume, beep.Callback(func() { go music.Finished(p) })))
return music, nil
}
func (p *AudioPlayer) openResource(name string) (beep.StreamSeekCloser, beep.Format, error) {
path := fmt.Sprintf("%s%s", p.prefix, name)
audio, err := p.resources.OpenResource(path)
if err != nil {
return nil, beep.Format{}, err
}
stream, format, err := mp3.Decode(audio)
if err != nil {
return nil, beep.Format{}, err
}
return stream, format, nil
}
type Music struct {
Name string
AutoRepeat bool
Stream beep.StreamCloser
Volume *effects.Volume
OnFinished func()
}
func (m *Music) Stop() {
m.AutoRepeat = false
m.Stream.Close()
}
func (m Music) Finished(player *AudioPlayer) {
m.Stream.Close()
if m.AutoRepeat {
player.PlayMusic(m.Name, func(m *Music) { m.AutoRepeat = true })
}
onFinished := m.OnFinished
if onFinished != nil {
onFinished()
}
}
type Sample struct {
*beep.Buffer
SampleRate beep.SampleRate
}
func (s Sample) Stream() beep.Streamer {
return s.Buffer.Streamer(0, s.Buffer.Len())
}
type streamCloser struct {
beep.StreamCloser
}
func (c *streamCloser) Err() error {
if c.StreamCloser == nil {
return io.EOF
}
return c.StreamCloser.Err()
}
func (c *streamCloser) Stream(samples [][2]float64) (n int, ok bool) {
if c.StreamCloser == nil {
return 0, false
}
return c.StreamCloser.Stream(samples)
}
func (c *streamCloser) Close() error {
c.StreamCloser = nil
return nil
}
func Resample(stream beep.Streamer, expected, actual beep.SampleRate) beep.Streamer {
if expected == actual {
return stream
}
return beep.Resample(3, actual, expected, stream)
}

View File

@ -123,13 +123,35 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
return false return false
} }
checkGameOver := func() {
if r.Level.GameOver {
r.Highscore = r.updateHighscore()
if r.Highscore {
r.app.Audio.PlaySample("level_new_high_score.mp3")
} else {
r.app.Audio.PlaySample("level_game_over.mp3")
}
}
}
switch e := e.(type) { switch e := e.(type) {
case *ui.KeyDownEvent: case *ui.KeyDownEvent:
dir, ok := r.Controls[e.Key] dir, ok := r.Controls[e.Key]
if ok { if ok {
stars, lives := r.Level.StarsCollected, r.Level.Lives
r.Level.MovePlayer(dir) r.Level.MovePlayer(dir)
if r.Level.GameOver { switch {
r.Highscore = r.updateHighscore() case r.Level.StarsCollected > stars:
r.app.Audio.PlaySample("player_collect_star.mp3")
case r.Level.Lives < lives:
r.app.Audio.PlaySample("player_hurt.mp3")
case r.Level.Lives > lives:
r.app.Audio.PlaySample("player_collect_heart.mp3")
}
r.app.Audio.PlaySample("player_move.mp3")
checkGameOver()
if r.Level.StarsCollected == r.Level.Stars {
r.app.Audio.PlaySample("level_completed.mp3")
} }
} }
} }
@ -149,9 +171,8 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
r.Level.DestroyMonster(pos) r.Level.DestroyMonster(pos)
jumped = append(jumped, pos) jumped = append(jumped, pos)
r.Level.DecrementLive() r.Level.DecrementLive()
if r.Level.GameOver { r.app.Audio.PlaySample("player_hurt.mp3")
r.Highscore = r.updateHighscore() checkGameOver()
}
continue continue
} }
if animation.Frame < 50 { // after 50 frames the animation has finished if animation.Frame < 50 { // after 50 frames the animation has finished
@ -177,6 +198,7 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
r.Level.MonsterTargets[pos] = target r.Level.MonsterTargets[pos] = target
r.MovingMonsters.Frame(pos) r.MovingMonsters.Frame(pos)
jumping = append(jumping, pos) jumping = append(jumping, pos)
r.app.Audio.PlaySample("monster_jump.mp3")
break break
} }
} }

View File

@ -29,6 +29,8 @@ func (c *center) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.P
type mainMenu struct { type mainMenu struct {
ui.StackPanel ui.StackPanel
music *Music
} }
func newMainMenu(app *appContext, ctx ui.Context) ui.Control { func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
@ -37,7 +39,8 @@ func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
menu.Add("Play", func(ctx ui.Context) { menu.Add("Play", func(ctx ui.Context) {
app.Play(ctx) app.Play(ctx)
}) })
if app.Score.Current.Difficulty > 0 { resume := app.Score.Current.Difficulty > 0
if resume {
menu.Add("Resume", func(ctx ui.Context) { app.PlayResume(ctx) }) menu.Add("Resume", func(ctx ui.Context) { app.PlayResume(ctx) })
} }
menu.Add("Highscores", func(ctx ui.Context) { app.ShowHighscores(ctx) }) menu.Add("Highscores", func(ctx ui.Context) { app.ShowHighscores(ctx) })
@ -45,7 +48,14 @@ func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
menu.Add("Credits", func(ctx ui.Context) { app.ShowCredits(ctx) }) menu.Add("Credits", func(ctx ui.Context) { app.ShowCredits(ctx) })
menu.Add("Quit", func(ctx ui.Context) { ctx.Quit() }) menu.Add("Quit", func(ctx ui.Context) { ctx.Quit() })
menu.Activate(1) // play if resume {
menu.Activate(2) // resume
} else {
menu.Activate(1) // play
}
menu.ActiveChanged.AddHandlerEmpty((func(ui.Context) {
app.MenuInteraction()
}))
ctx.Animate() ctx.Animate()
return Center(&mainMenu{ return Center(&mainMenu{

View File

@ -11,6 +11,8 @@ type Menu struct {
active int active int
buttons []*MenuButton buttons []*MenuButton
ActiveChanged ui.Events
} }
func NewMenu() *Menu { func NewMenu() *Menu {
@ -25,7 +27,7 @@ func (m *Menu) Activate(i int) {
if len(m.buttons) == 0 || i < 0 { if len(m.buttons) == 0 || i < 0 {
return return
} }
m.updateActiveButton(i % len(m.buttons)) m.updateActiveButton(nil, i%len(m.buttons))
} }
func (m *Menu) Add(text string, click func(ui.Context)) { func (m *Menu) Add(text string, click func(ui.Context)) {
@ -54,29 +56,33 @@ func (m *Menu) Handle(ctx ui.Context, e ui.Event) bool {
onEscape(ctx) onEscape(ctx)
} }
case ui.KeyDown: case ui.KeyDown:
m.updateActiveButton((m.active + 1) % len(m.buttons)) m.updateActiveButton(ctx, (m.active+1)%len(m.buttons))
case ui.KeyUp: case ui.KeyUp:
m.updateActiveButton((m.active + len(m.buttons) - 1) % len(m.buttons)) m.updateActiveButton(ctx, (m.active+len(m.buttons)-1)%len(m.buttons))
case ui.KeyEnter: case ui.KeyEnter:
m.buttons[m.active].InvokeClick(ctx) m.buttons[m.active].InvokeClick(ctx)
} }
case *ui.MouseMoveEvent: case *ui.MouseMoveEvent:
for i, button := range m.buttons { for i, button := range m.buttons {
if button.IsOver() { if button.IsOver() {
m.updateActiveButton(i) m.updateActiveButton(ctx, i)
break break
} }
} }
m.updateActiveButton(m.active) m.updateActiveButton(ctx, m.active)
} }
return false return false
} }
func (m *Menu) updateActiveButton(active int) { func (m *Menu) updateActiveButton(ctx ui.Context, active int) {
change := m.active != active
m.active = active m.active = active
for i, btn := range m.buttons { for i, btn := range m.buttons {
btn.Over = i == m.active btn.Over = i == m.active
} }
if change && ctx != nil {
m.ActiveChanged.Notify(ctx, nil)
}
} }
type MenuButton struct { type MenuButton struct {

View File

@ -207,6 +207,7 @@ func (s *settings) Handle(ctx ui.Context, e ui.Event) bool {
switch e.Key { switch e.Key {
case ui.KeyEscape: case ui.KeyEscape:
s.SelectingCustom = 0 s.SelectingCustom = 0
s.app.MenuInteraction()
return true return true
} }
key, ok := supportedCustomKeys[e.Key] key, ok := supportedCustomKeys[e.Key]
@ -229,33 +230,88 @@ func (s *settings) Handle(ctx ui.Context, e ui.Event) bool {
s.SelectedLayout = 2 s.SelectedLayout = 2
s.app.Settings.Controls.Type = controlsTypeCustom s.app.Settings.Controls.Type = controlsTypeCustom
} }
s.app.MenuInteraction()
} }
return true return true
} }
switch e.Key { switch e.Key {
case ui.KeyEscape: case ui.KeyEscape:
s.app.ShowMainMenu(ctx) s.app.ShowMainMenu(ctx)
s.app.MenuInteraction()
return true return true
case ui.KeyLeft: case ui.KeyLeft:
s.ActiveLayout = (s.ActiveLayout + 2) % 3 s.setActiveLayout(s.ActiveLayout - 1)
case ui.KeyRight: case ui.KeyRight:
s.ActiveLayout = (s.ActiveLayout + 1) % 3 s.setActiveLayout(s.ActiveLayout + 1)
case ui.KeyEnter: case ui.KeyEnter:
switch s.ActiveLayout { s.selectLayout()
case 0: }
s.SelectedLayout = 0 case *ui.MouseMoveEvent:
s.app.Settings.Controls.Type = controlsTypeWASD if s.SelectingCustom == 0 {
case 1: layout := s.isOverLayout(ctx, e.Pos())
s.SelectedLayout = 1 if layout > -1 {
s.app.Settings.Controls.Type = controlsTypeArrows s.setActiveLayout(layout)
case 2: }
s.SelectingCustom = 1 }
case *ui.MouseButtonDownEvent:
if s.SelectingCustom == 0 {
if e.Button == ui.MouseButtonLeft {
layout := s.isOverLayout(ctx, e.Pos())
if layout > -1 {
s.setActiveLayout(layout) // to be sure, mouse move should've set this already
s.selectLayout()
}
} }
} }
} }
return false return false
} }
func (s *settings) selectLayout() {
s.app.MenuInteraction()
switch s.ActiveLayout {
case 0:
s.SelectedLayout = 0
s.app.Settings.Controls.Type = controlsTypeWASD
case 1:
s.SelectedLayout = 1
s.app.Settings.Controls.Type = controlsTypeArrows
case 2:
s.SelectingCustom = 1
}
}
func (s *settings) isOverLayout(ctx ui.Context, mouse geom.PointF32) int {
bounds := s.Bounds()
center := bounds.Center()
width := bounds.Dx()
scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*width)
font := ctx.Fonts().Font("default")
for i := 0; i < 3; i++ {
left := (.04 + .32*float32(i)) * width
right := left + .28*width
top := center.Y - 2*font.Height()
bottom := center.Y + scale*keyboardLayoutTextureWidth
if mouse.In(geom.RectF32(left, top, right, bottom)) {
return i
}
}
return -1
}
func (s *settings) setActiveLayout(layout int) {
layout = (layout + 3) % 3
change := layout != s.ActiveLayout
s.ActiveLayout = (layout + 3) % 3
if change {
s.app.MenuInteraction()
}
}
func (s *settings) Render(ctx ui.Context) { func (s *settings) Render(ctx ui.Context) {
bounds := s.Bounds() bounds := s.Bounds()
center := bounds.Center() center := bounds.Center()