From 842c41fdcadd1a92bbf80ebca51f421fce271ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Ryb=C3=A1rsky?= Date: Thu, 29 Aug 2024 21:12:18 +0200 Subject: [PATCH] Add goroutines to map renderer --- main.go | 123 +++++++++++++++++++++++++++++++++----------------- map.go | 76 +++++++++++++++++++++++-------- player.go | 6 +++ playerData.go | 2 +- profiler.go | 15 ++++++ 5 files changed, 160 insertions(+), 62 deletions(-) diff --git a/main.go b/main.go index 163caca..1b3c609 100644 --- a/main.go +++ b/main.go @@ -31,13 +31,14 @@ const ( ReloadCooldown = 16 Debug = false - MapWindow = true + MapWindow = false RenderGameObjects = true ProfilerOn = true + ProfilerInterval = 100 JoyStickDeadZone = 8000 - DoAllKeymapsPlayers = true + DoAllKeymapsPlayers = false DoJoyStickPlayers = true DoKeymapPlayer = true ) @@ -71,45 +72,85 @@ func main() { mapWindow, mapSurface = setupMapWindowAndSurface() } running := true + var totalRenderTime, totalScalingTime, totalFrameTime, frameCount uint64 + + // Delta time management + var prevTime = sdl.GetTicks64() + const maxDeltaTime uint64 = 1000 / 60 // max 60 FPS, ~16ms per frame + for running { - frameStart := sdl.GetTicks64() + currentTime := sdl.GetTicks64() + deltaTime := currentTime - prevTime + prevTime = currentTime + + // Catch up in case of a large delta time + for deltaTime > maxDeltaTime { + deltaTime -= maxDeltaTime + + // Run multiple logic ticks if deltaTime is too large + runGameLogic(players, gameMap, bases, bullets, bulletParticles) + } + for playerIndex, player := range *players { - // Delay to achieve roughly 60 FPS if player.local { - running = doPlayerFrame(uint8(playerIndex), player, players, gameMap, bases, bullets, bulletParticles) + running := doPlayerFrame(uint8(playerIndex), player, players, gameMap, bases, bullets, bulletParticles) if !running { break } } } + + // Run logic for the remaining delta time + runGameLogic(players, gameMap, bases, bullets, bulletParticles) + + // Render logic if MapWindow { - gameMap.render(mapRendererRect, mapSurface) - for _, bullet := range *bullets { - (*bullet).render(mapRendererRect, mapSurface) - } - for _, base := range *bases { - (*base).render(mapRendererRect, mapSurface) - } - for _, playerLoop := range *players { - (*playerLoop).render(mapRendererRect, mapSurface) - } - for _, bulletParticle := range *bulletParticles { - bulletParticle.render(mapRendererRect, mapSurface) - } - adjustWindow(mapWindow, mapSurface) - } - //now tick world - for _, bullet := range *bullets { - (*bullet).tick(gameMap, bulletParticles, bullets, players) - } - for _, base := range *bases { - (*base).tick(players) - } - for _, bulletParticle := range *bulletParticles { - bulletParticle.tick(bulletParticles) + // Profile rendering (aggregate all renderings) + totalRenderTime += profileSection(func() { + gameMap.render(mapRendererRect, mapSurface) + for _, bullet := range *bullets { + (*bullet).render(mapRendererRect, mapSurface) + } + for _, base := range *bases { + (*base).render(mapRendererRect, mapSurface) + } + for _, playerLoop := range *players { + (*playerLoop).render(mapRendererRect, mapSurface) + } + for _, bulletParticle := range *bulletParticles { + bulletParticle.render(mapRendererRect, mapSurface) + } + }) + totalScalingTime += profileSection(func() { + adjustWindow(mapWindow, mapSurface) + }) } - enforceFrameRate(frameStart, 60) + frameEnd := sdl.GetTicks64() + totalFrameTime += frameEnd - currentTime + frameCount++ + + // Log profiling information every 1000 frames + if frameCount%ProfilerInterval == 0 && ProfilerOn { + logMapProfilingInfo(totalRenderTime, totalScalingTime, totalFrameTime, frameCount) + resetMapProfilingCounters(&totalRenderTime, &totalScalingTime, &totalFrameTime, &frameCount) + } + + enforceFrameRate(currentTime, 60) + } +} + +// Separate function to handle game logic +func runGameLogic(players *[]*Player, gameMap *GameMap, bases *[]*Base, bullets *[]*Bullet, bulletParticles *[]*BulletParticle) { + // Tick world + for _, bullet := range *bullets { + (*bullet).tick(gameMap, bulletParticles, bullets, players) + } + for _, base := range *bases { + (*base).tick(players) + } + for _, bulletParticle := range *bulletParticles { + bulletParticle.tick(bulletParticles) } } @@ -130,22 +171,20 @@ func initPlayer(playerIndex uint8, player *Player) { func doPlayerFrame(playerIndex uint8, player *Player, players *[]*Player, gameMap *GameMap, bases *[]*Base, bullets *[]*Bullet, bulletParticles *[]*BulletParticle) bool { running := true var ( - totalHandleEventsTime, totalRenderTime, totalFrameTime uint64 - totalScalingTime, frameCount uint64 - isShooting, shouldContinue bool + isShooting, shouldContinue bool ) frameStart := sdl.GetTicks64() // Profile handleEvents - totalHandleEventsTime += profileSection(func() { + player.totalHandleEventsTime += profileSection(func() { running = handleEvents(player.window, player.logicalSurface) keyboard := sdl.GetKeyboardState() - shouldContinue, isShooting = handleInput(keyboard, bullets, player, gameMap, player.playSurface.Format, players) + shouldContinue, isShooting = handleInput(keyboard, bullets, player, gameMap, players) running = running && shouldContinue }) // Profile rendering (aggregate all renderings) - totalRenderTime += profileSection(func() { + player.totalRenderTime += profileSection(func() { player.track(player.camera) gameMap.render(player.camera, player.playSurface) @@ -169,18 +208,18 @@ func doPlayerFrame(playerIndex uint8, player *Player, players *[]*Player, gameMa }) // Profile window adjustments - totalScalingTime += profileSection(func() { + player.totalScalingTime += profileSection(func() { adjustWindow(player.window, player.logicalSurface) }) frameEnd := sdl.GetTicks64() - totalFrameTime += frameEnd - frameStart - frameCount++ + player.totalFrameTime += frameEnd - frameStart + player.frameCount++ // Log profiling information every 1000 frames - if frameCount%1000 == 0 && ProfilerOn && playerIndex == 0 { - logProfilingInfo(totalHandleEventsTime, totalRenderTime, totalScalingTime, totalFrameTime, frameCount) - resetProfilingCounters(&totalHandleEventsTime, &totalRenderTime, &totalScalingTime, &totalFrameTime, &frameCount) + if player.frameCount%ProfilerInterval == 0 && ProfilerOn && playerIndex == 0 { + logProfilingInfo(player.totalHandleEventsTime, player.totalRenderTime, player.totalScalingTime, player.totalFrameTime, player.frameCount) + resetProfilingCounters(&player.totalHandleEventsTime, &player.totalRenderTime, &player.totalScalingTime, &player.totalFrameTime, &player.frameCount) } return running } diff --git a/map.go b/map.go index 9202936..bf83857 100644 --- a/map.go +++ b/map.go @@ -4,6 +4,7 @@ import ( "github.com/aquilax/go-perlin" "github.com/veandco/go-sdl2/sdl" "image/color" + "sync" "time" ) @@ -48,26 +49,49 @@ func (gameMap *GameMap) getTileColor(x int32, y int32) color.Color { } func (gameMap *GameMap) createGameMap(width int32, height int32) { + // Initialize the 2D slice for game tiles gameMap.tiles = make([][]uint8, width) + + // Create Perlin noise generators perlinNoiseRock := perlin.NewPerlin(2, 4, 8, time.Now().Unix()) perlinNoiseSoil := perlin.NewPerlin(2, 8, 4, time.Now().Unix()) + + // WaitGroup to synchronize goroutines + var wg sync.WaitGroup + + // Loop over the rows for i := range gameMap.tiles { - gameMap.tiles[i] = make([]uint8, height) - for j := range gameMap.tiles[i] { - perlinRock := perlinNoiseRock.Noise2D(float64(i)/float64(width)*6, float64(j)/float64(height)*6) - if perlinRock > 0.2 { - gameMap.tiles[i][j] = 1 - } else { - perlinSoil := perlinNoiseSoil.Noise2D(float64(i)/float64(width)*10, float64(j)/float64(height)*10) - if perlinSoil >= 0 { - gameMap.tiles[i][j] = 2 + // Increment WaitGroup counter for each goroutine + wg.Add(1) + + // Launch a goroutine for each row + go func(i int) { + defer wg.Done() // Signal that this goroutine is done + + // Initialize the row + gameMap.tiles[i] = make([]uint8, height) + + // Process each tile in the row + for j := range gameMap.tiles[i] { + perlinRock := perlinNoiseRock.Noise2D(float64(i)/float64(width)*6, float64(j)/float64(height)*6) + if perlinRock > 0.2 { + gameMap.tiles[i][j] = 1 } else { - gameMap.tiles[i][j] = 3 + perlinSoil := perlinNoiseSoil.Noise2D(float64(i)/float64(width)*10, float64(j)/float64(height)*10) + if perlinSoil >= 0 { + gameMap.tiles[i][j] = 2 + } else { + gameMap.tiles[i][j] = 3 + } } } - - } + }(i) } + + // Wait for all goroutines to finish + wg.Wait() + + // Set the dimensions of the game map gameMap.width = width gameMap.height = height } @@ -77,17 +101,31 @@ func (gameMap *GameMap) render(camera *sdl.Rect, surface *sdl.Surface) { fromY := camera.Y toX := camera.X + camera.W toY := camera.Y + camera.H + + // Create a WaitGroup to wait for all goroutines to finish + var wg sync.WaitGroup + + // Process columns instead of individual pixels for x := fromX; x < toX; x++ { - for y := fromY; y < toY; y++ { - cameraY := y - camera.Y - cameraX := x - camera.X - pixelColor := gameMap.getTileColor(x, y) - surface.Set(int(cameraX), int(cameraY), pixelColor) + // Increment the WaitGroup counter + wg.Add(1) - } + // Launch a goroutine for each column + go func(x int32) { + defer wg.Done() // Decrement the counter when the goroutine completes + + for y := fromY; y < toY; y++ { + cameraY := y - camera.Y + cameraX := x - camera.X + pixelColor := gameMap.getTileColor(x, y) + surface.Set(int(cameraX), int(cameraY), pixelColor) + } + }(x) } -} + // Wait for all goroutines to finish + wg.Wait() +} func (gameMap *GameMap) checkCollision(posX, posY int32) uint8 { //0 no collision //1 destructible collision diff --git a/player.go b/player.go index 8a100a9..3a7ac39 100644 --- a/player.go +++ b/player.go @@ -35,6 +35,12 @@ type Player struct { playSurfaceTargetRect *sdl.Rect HUDSurfaceTargetRect *sdl.Rect + + totalHandleEventsTime uint64 + totalRenderTime uint64 + totalFrameTime uint64 + totalScalingTime uint64 + frameCount uint64 } func (player *Player) track(camera *sdl.Rect) { diff --git a/playerData.go b/playerData.go index e897bb4..447202b 100644 --- a/playerData.go +++ b/playerData.go @@ -285,7 +285,7 @@ func initPlayerColors() { } } -func handleInput(keyboard []uint8, bullets *[]*Bullet, player *Player, gameMap *GameMap, format *sdl.PixelFormat, players *[]*Player) (bool, bool) { +func handleInput(keyboard []uint8, bullets *[]*Bullet, player *Player, gameMap *GameMap, players *[]*Player) (bool, bool) { if !player.local || player.shields <= 0 || player.shields > MaxShields { return true, false } diff --git a/profiler.go b/profiler.go index 1e71fbb..ea3f35f 100644 --- a/profiler.go +++ b/profiler.go @@ -21,6 +21,14 @@ func logProfilingInfo(handleEventsTime, renderTime, scaleTime, frameTime, frameC fmt.Print("\n") } +func logMapProfilingInfo(renderTime, scaleTime, frameTime, frameCount uint64) { + floatFrame := float64(frameCount) + fmt.Printf("Average map render time: %f ms\n", float64(renderTime)/floatFrame) + fmt.Printf("Average map scaling time: %f ms\n", float64(scaleTime)/floatFrame) + fmt.Printf("Average full render time: %f ms\n", float64(frameTime)/floatFrame) + fmt.Print("\n") +} + func resetProfilingCounters(handleEventsTime, renderTime, scaleTime, frameTime *uint64, frameCount *uint64) { *handleEventsTime = 0 *renderTime = 0 @@ -29,6 +37,13 @@ func resetProfilingCounters(handleEventsTime, renderTime, scaleTime, frameTime * *frameCount = 0 } +func resetMapProfilingCounters(renderTime, scaleTime, frameTime *uint64, frameCount *uint64) { + *renderTime = 0 + *scaleTime = 0 + *frameTime = 0 + *frameCount = 0 +} + func enforceFrameRate(frameStart uint64, targetFPS int) { frameDuration := 1000 / targetFPS elapsed := sdl.GetTicks64() - frameStart