FPS counter in settings

This commit is contained in:
2025-11-19 09:57:16 -07:00
parent 1098e383ce
commit a7e6f4e0bf
6 changed files with 279 additions and 98 deletions

View File

@@ -1,6 +1,7 @@
package game
import (
"fmt"
"image/color"
"time"
@@ -13,10 +14,7 @@ import (
"github.com/atridad/BigFeelings/internal/ui/menu"
)
// ============================================================
// CONFIGURATION
// Tweak these values to change game settings
// ============================================================
// Game settings.
const (
ScreenWidth = 960
@@ -25,11 +23,51 @@ const (
WindowTitle = "Big Feelings"
)
// FPS cap options.
type FPSCap int
const (
FPSCap60 FPSCap = iota
FPSCap120
FPSCapUncapped
fpsCapCount
)
func (f FPSCap) TPS() int {
switch f {
case FPSCap60:
return 60
case FPSCap120:
return 120
case FPSCapUncapped:
return -1
default:
return 60
}
}
func (f FPSCap) String() string {
switch f {
case FPSCap60:
return "60 FPS"
case FPSCap120:
return "120 FPS"
case FPSCapUncapped:
return "Uncapped"
default:
return "60 FPS"
}
}
func (f *FPSCap) Cycle() {
*f = (*f + 1) % fpsCapCount
}
var (
backgroundColor = color.NRGBA{R: 0, G: 0, B: 0, A: 255}
)
// Hero configuration
// Hero settings.
const (
heroStartX = ScreenWidth / 2
heroStartY = ScreenHeight / 2
@@ -44,26 +82,28 @@ var (
heroColor = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
)
// HUD configuration
// HUD settings.
const (
hudX = ScreenWidth - 220
hudY = 20
)
// Stamina bar colors
// HUD colors.
var (
staminaNormalColor = color.NRGBA{R: 0, G: 255, B: 180, A: 255}
staminaLowColor = color.NRGBA{R: 255, G: 60, B: 60, A: 255}
fpsGoodColor = color.NRGBA{R: 120, G: 255, B: 120, A: 255}
fpsWarnColor = color.NRGBA{R: 255, G: 210, B: 100, A: 255}
fpsPoorColor = color.NRGBA{R: 255, G: 120, B: 120, A: 255}
)
const (
staminaLowThreshold = 0.2
fpsWarnThreshold = 0.85
fpsPoorThreshold = 0.6
fpsSampleWindow = time.Second
)
// ============================================================
// TYPES
// ============================================================
type gameState int
const (
@@ -84,25 +124,26 @@ type Game struct {
}
type state struct {
hero *hero.Hero
hud hud.Overlay
bounds hero.Bounds
lastTick time.Time
pauseMenu *menu.PauseMenu
gameState gameState
hero *hero.Hero
hud hud.Overlay
bounds hero.Bounds
lastTick time.Time
pauseMenu *menu.PauseMenu
gameState gameState
fpsEnabled bool
fpsFrames int
fpsAccumulator time.Duration
fpsValue float64
fpsCap FPSCap
}
// ============================================================
// INITIALIZATION
// ============================================================
func New() *Game {
return &Game{state: newState()}
}
func newState() *state {
now := time.Now()
return &state{
s := &state{
hero: hero.New(hero.Config{
StartX: heroStartX,
StartY: heroStartY,
@@ -122,16 +163,18 @@ func newState() *state {
Width: ScreenWidth,
Height: ScreenHeight,
},
lastTick: now,
pauseMenu: menu.NewPauseMenu(),
gameState: statePlaying,
lastTick: now,
pauseMenu: menu.NewPauseMenu(),
gameState: statePlaying,
fpsEnabled: false,
fpsCap: FPSCap60,
}
s.pauseMenu.SetFPSMonitor(&s.fpsEnabled)
s.pauseMenu.SetFPSCap(&s.fpsCap)
ebiten.SetTPS(s.fpsCap.TPS())
return s
}
// ============================================================
// INPUT
// ============================================================
func readControls() controls {
return controls{
Left: ebiten.IsKeyPressed(ebiten.KeyArrowLeft) || ebiten.IsKeyPressed(ebiten.KeyA),
@@ -142,11 +185,15 @@ func readControls() controls {
}
}
// ============================================================
// UPDATE
// ============================================================
func (g *Game) Update() error {
// Update TPS if FPS cap changed.
currentTPS := g.state.fpsCap.TPS()
if currentTPS < 0 {
ebiten.SetTPS(ebiten.SyncWithFPS)
} else {
ebiten.SetTPS(currentTPS)
}
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
if g.state.gameState == statePlaying {
g.state.gameState = statePaused
@@ -157,6 +204,12 @@ func (g *Game) Update() error {
}
}
// Track FPS.
now := time.Now()
if !g.state.lastTick.IsZero() {
g.state.trackFPS(now.Sub(g.state.lastTick))
}
if g.state.gameState == statePlaying {
g.state.update(readControls())
} else if g.state.gameState == statePaused {
@@ -188,9 +241,20 @@ func (s *state) update(input controls) {
}, dt, s.bounds)
}
// ============================================================
// RENDERING
// ============================================================
func (s *state) trackFPS(delta time.Duration) {
if !s.fpsEnabled {
return
}
s.fpsAccumulator += delta
s.fpsFrames++
if s.fpsAccumulator >= fpsSampleWindow {
s.fpsValue = float64(s.fpsFrames) / s.fpsAccumulator.Seconds()
s.fpsAccumulator = 0
s.fpsFrames = 0
}
}
func (g *Game) Draw(screen *ebiten.Image) {
g.state.draw(screen)
@@ -211,13 +275,46 @@ func (s *state) draw(screen *ebiten.Image) {
Level: s.hero.Stamina,
Color: staminaColor,
}
s.hud.Draw(screen, []status.Meter{staminaMeter})
meters := []status.Meter{staminaMeter}
if s.fpsEnabled {
// Color based on target FPS (60).
ratio := s.fpsValue / float64(TargetTPS)
fpsColor := fpsGoodColor
switch {
case ratio < fpsPoorThreshold:
fpsColor = fpsPoorColor
case ratio < fpsWarnThreshold:
fpsColor = fpsWarnColor
}
fpsMeter := status.Meter{
Label: fmt.Sprintf("Framerate: %3.0f FPS", s.fpsValue),
Base: -1, // Negative base means text-only display.
Level: 0,
Color: fpsColor,
}
meters = append(meters, fpsMeter)
}
s.hud.Draw(screen, meters)
if s.gameState == statePaused {
s.pauseMenu.Draw(screen, ScreenWidth, ScreenHeight)
}
}
func clampFloat(value, min, max float64) float64 {
if value < min {
return min
}
if value > max {
return max
}
return value
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return ScreenWidth, ScreenHeight
}