package main import ( _ "embed" "fmt" "github.com/veandco/go-sdl2/mix" "github.com/veandco/go-sdl2/sdl" "gitlab.com/rhogenson/pongo/numbers" "math" "os" "time" ) const ( screenWidth = 1920 screenHeight = 1080 ballSpeed = 5 paddleSpeed = 5 ) func rectRight(r *sdl.FRect) float32 { return r.X + r.W } func rectBottom(r *sdl.FRect) float32 { return r.Y + r.H } type vector2 struct { x, y float32 } func (v vector2) add(w vector2) vector2 { return vector2{v.x + w.x, v.y + w.y} } type ball struct { pos vector2 velocity vector2 } func (b *ball) bounds() *sdl.FRect { const ballSize = 30 return &sdl.FRect{ X: b.pos.x - ballSize/2, Y: b.pos.y - ballSize/2, W: ballSize, H: ballSize, } } func (b *ball) predictIntersection(targetX float32) (float32, bool) { if b.velocity.x <= 0 { return 0, false } x := b.pos.x y := b.pos.y velocity := b.velocity dist := targetX - x prediction := y + velocity.y*dist/velocity.x if prediction < 0 { bounces := int(-prediction / screenHeight) if bounces%2 == 0 { return -prediction - float32(screenHeight*bounces), true } return prediction + float32(screenHeight*(bounces+1)), true } if prediction > screenHeight { bounces := int(prediction / screenHeight) if bounces%2 == 0 { return prediction - float32(screenHeight*bounces), true } return -prediction + float32(screenHeight*(bounces+1)), true } return prediction, true } type side int const ( left side = iota right ) func (b *ball) reset(side side) { b.pos = vector2{screenWidth / 2, screenHeight / 2} b.velocity = vector2{x: ballSpeed} if side == left { b.velocity.x = -b.velocity.x } } type paddle struct { pos float32 side side } func (p *paddle) bounds() *sdl.FRect { const ( paddleHeight = 200 thickness = 10 padding = 100 ) r := &sdl.FRect{ X: padding - thickness/2, Y: p.pos - paddleHeight/2, W: thickness, H: paddleHeight, } if p.side == right { r.X = screenWidth - padding - thickness/2 } return r } //go:embed ping.wav var pingWAV []byte func pong() error { if err := sdl.Init(sdl.INIT_AUDIO & sdl.INIT_VIDEO & sdl.INIT_EVENTS); err != nil { return err } defer sdl.Quit() if err := mix.OpenAudio(44100, mix.DEFAULT_FORMAT, 2, 4096); err != nil { return err } defer mix.CloseAudio() ping, err := mix.QuickLoadWAV(pingWAV) if err != nil { return err } defer ping.Free() window, renderer, err := sdl.CreateWindowAndRenderer(screenWidth, screenHeight, sdl.WINDOW_OPENGL&sdl.WINDOW_BORDERLESS) if err != nil { return err } defer window.Destroy() defer renderer.Destroy() upPressed := false downPressed := false playerScore := 0 aiScore := 0 var ball ball ball.reset(left) paddle1 := paddle{pos: screenHeight / 2, side: left} paddle2 := paddle{pos: screenHeight / 2, side: right} lastFrameTime := time.Now() accumulator := time.Duration(0) renderRate := time.NewTicker(time.Second / 60) defer renderRate.Stop() for frameStart := range renderRate.C { for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { switch t := event.(type) { case *sdl.QuitEvent: return nil case *sdl.KeyboardEvent: switch t.Keysym.Sym { case sdl.K_UP: upPressed = t.State == sdl.PRESSED case sdl.K_DOWN: downPressed = t.State == sdl.PRESSED } } } accumulator += frameStart.Sub(lastFrameTime) lastFrameTime = frameStart const simulationSpeed = time.Second / 120 for ; accumulator >= simulationSpeed; accumulator -= simulationSpeed { switch { case upPressed && paddle1.bounds().Y >= 0: paddle1.pos -= paddleSpeed case downPressed && rectBottom(paddle1.bounds()) <= screenHeight: paddle1.pos += paddleSpeed } if prediction, ok := ball.predictIntersection(paddle2.bounds().X); ok { switch { case paddle2.pos <= prediction-paddleSpeed && rectBottom(paddle2.bounds()) <= screenHeight: paddle2.pos += paddleSpeed case paddle2.pos >= prediction+paddleSpeed && paddle2.bounds().Y >= 0: paddle2.pos -= paddleSpeed } } ball.pos = ball.pos.add(ball.velocity) switch { case ball.bounds().HasIntersection(paddle1.bounds()) && ball.velocity.x <= 0: if _, err := ping.Play(-1, 0); err != nil { fmt.Fprintln(os.Stderr, err) } d := (ball.pos.y - paddle1.pos) / (paddle1.bounds().H/2 + ball.bounds().H) if d < 0 { d = -d } angle := d * 75 * 2 * math.Pi / 360 if ball.pos.y < paddle1.pos { angle = -angle } speed := ballSpeed * (1 + 4*d) ball.velocity = vector2{speed * float32(math.Cos(float64(angle))), speed * float32(math.Sin(float64(angle)))} case ball.bounds().HasIntersection(paddle2.bounds()) && ball.velocity.x >= 0: if _, err := ping.Play(-1, 0); err != nil { fmt.Fprintln(os.Stderr, err) } d := (ball.pos.y - paddle2.pos) / (paddle2.bounds().H/2 + ball.bounds().H) if d < 0 { d = -d } angle := d * 75 * 2 * math.Pi / 360 if ball.pos.y < paddle2.pos { angle = -angle } angle += math.Pi speed := ballSpeed * (1 + 4*d) ball.velocity = vector2{speed * float32(math.Cos(float64(angle))), speed * float32(math.Sin(float64(angle)))} case rectRight(ball.bounds()) <= 0: aiScore++ ball.reset(right) case ball.bounds().X >= screenWidth: playerScore++ ball.reset(left) case ball.bounds().Y <= 0 && ball.velocity.y < 0 || rectBottom(ball.bounds()) >= screenHeight && ball.velocity.y > 0: if _, err := ping.Play(-1, 0); err != nil { fmt.Fprintln(os.Stderr, err) } ball.velocity.y = -ball.velocity.y } } if err := renderer.SetDrawColor(0, 0, 0, 255); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.FillRect(nil); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.SetDrawColor(130, 130, 130, 255); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.FillRect(&sdl.Rect{X: screenWidth / 2, Y: 0, W: 1, H: screenHeight}); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.SetDrawColor(245, 245, 245, 255); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.FillRectF(paddle1.bounds()); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.FillRectF(paddle2.bounds()); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.FillRectF(ball.bounds()); err != nil { fmt.Fprintln(os.Stderr, err) } if err := numbers.Draw(renderer, playerScore, 150, 30); err != nil { fmt.Fprintln(os.Stderr, err) } if err := numbers.Draw(renderer, aiScore, 1300, 30); err != nil { fmt.Fprintln(os.Stderr, err) } renderer.Present() } panic("unreachable") } func main() { if err := pong(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }