v2 / examples / gg / pong / pong.v
346 lines · 306 sloc · 8.75 KB · e2e5cf8db56f3562c7baa735061690be936bdf3e
Raw
1// vtest build: !openbsd
2module main
3
4import gg
5import rand
6import sokol.audio
7import math
8
9const paddle_speed_base = f32(400.0)
10const ball_speed_base = f32(300.0)
11
12// Colors
13const color_bg = gg.Color{10, 15, 30, 255}
14const color_foreground = gg.Color{230, 230, 230, 255}
15const color_accent = gg.Color{0, 255, 100, 255}
16const color_secondary = gg.Color{0, 200, 80, 255}
17const color_text_dim = gg.Color{150, 150, 150, 200}
18const color_line = gg.Color{100, 100, 100, 150}
19const color_p1 = gg.Color{255, 50, 50, 255}
20const color_p2 = gg.Color{50, 100, 255, 255}
21
22struct SoundManager {
23mut:
24 ping []f32
25 pong []f32
26 buzz []f32
27}
28
29fn (mut sm SoundManager) init() {
30 sample_rate := f32(audio.sample_rate())
31
32 // Ping (Wall)
33 ping_duration := f32(0.1)
34 ping_frames := int(sample_rate * ping_duration)
35 for i in 0 .. ping_frames {
36 t := f32(i) / sample_rate
37 // Use a steeper exponential-like decay for a cleaner "pluck"
38 env := f32(math.pow(f32(1.0) - t / ping_duration, 3))
39 sm.ping << f32(0.3) * math.sinf(t * 800.0 * 2 * math.pi) * env
40 }
41 // Absolute silence buffer to prevent clicks
42 for _ in 0 .. 100 {
43 sm.ping << 0
44 }
45
46 // Pong (Paddle)
47 pong_duration := f32(0.1)
48 pong_frames := int(sample_rate * pong_duration)
49 for i in 0 .. pong_frames {
50 t := f32(i) / sample_rate
51 env := f32(math.pow(f32(1.0) - t / pong_duration, 3))
52 sm.pong << f32(0.3) * math.sinf(t * 400.0 * 2 * math.pi) * env
53 }
54 for _ in 0 .. 100 {
55 sm.pong << 0
56 }
57
58 // Buzz (Reset)
59 buzz_duration := f32(0.3)
60 buzz_frames := int(sample_rate * buzz_duration)
61 for i in 0 .. buzz_frames {
62 t := f32(i) / sample_rate
63 env := f32(math.pow(f32(1.0) - t / buzz_duration, 2))
64 // Add a harmonic for a more "buzzy" feel
65 val := f32(0.25) * math.sinf(t * 100.0 * 2 * math.pi) +
66 f32(0.1) * math.sinf(t * 200.0 * 2 * math.pi)
67 sm.buzz << val * env
68 }
69 for _ in 0 .. 200 {
70 sm.buzz << 0
71 }
72}
73
74struct App {
75mut:
76 ctx &gg.Context = unsafe { nil }
77 width int = 800
78 height int = 600
79 paddle_width f32
80 paddle_height f32
81 ball_size f32
82 p1_y f32
83 p2_y f32
84 ball_x f32
85 ball_y f32
86 ball_vx f32
87 ball_vy f32
88 p1_score int
89 p2_score int
90 paused bool
91 sounds SoundManager
92}
93
94fn (mut app App) update_sizes() {
95 size := app.ctx.window_size()
96 app.width = size.width
97 app.height = size.height
98
99 // Base scales on 800x600
100 scale_x := f32(app.width) / 800.0
101 scale_y := f32(app.height) / 600.0
102
103 app.paddle_width = 15.0 * scale_x
104 app.paddle_height = 80.0 * scale_y
105 app.ball_size = 15.0 * scale_x // Keep it circular based on width scale
106}
107
108fn (mut app App) reset_ball() {
109 app.ball_x = f32(app.width) / 2 - app.ball_size / 2
110 app.ball_y = f32(app.height) / 2 - app.ball_size / 2
111
112 scale_x := f32(app.width) / 800.0
113 mut vx := ball_speed_base * scale_x
114 if rand.intn(2) or { 0 } == 0 {
115 vx = -vx
116 }
117 app.ball_vx = vx
118 app.ball_vy = (f32(rand.intn(301) or { 150 } - 150)) * (f32(app.height) / 600.0)
119}
120
121fn (mut app App) reset_game() {
122 app.p1_score = 0
123 app.p2_score = 0
124 app.p1_y = f32(app.height) / 2 - app.paddle_height / 2
125 app.p2_y = f32(app.height) / 2 - app.paddle_height / 2
126 app.reset_ball()
127 app.paused = false
128}
129
130fn on_frame(data voidptr) {
131 mut app := unsafe { &App(data) }
132 app.ctx.begin()
133
134 // Draw dashed center line
135 line_segments := 20
136 segment_height := f32(app.height) / line_segments
137 for i in 0 .. line_segments {
138 if i % 2 == 0 {
139 app.ctx.draw_rect_filled(f32(app.width) / 2 - 1, i * segment_height, 2, segment_height,
140 color_line)
141 }
142 }
143
144 // Draw paddles
145 app.ctx.draw_rounded_rect_filled(20, app.p1_y, app.paddle_width, app.paddle_height, 5, color_p1)
146 app.ctx.draw_rounded_rect_filled(f32(app.width) - 20 - app.paddle_width, app.p2_y,
147 app.paddle_width, app.paddle_height, 5, color_p2)
148
149 // Draw ball
150 app.ctx.draw_circle_filled(app.ball_x + app.ball_size / 2, app.ball_y + app.ball_size / 2,
151 app.ball_size / 2, color_foreground)
152
153 // Draw ball speed (10m = 800px => 1m = 80px)
154 // We scale the meter definition with width
155 meter_px := 80.0 * (f32(app.width) / 800.0)
156 speed_px := math.sqrt(app.ball_vx * app.ball_vx + app.ball_vy * app.ball_vy)
157 speed_ms := speed_px / meter_px
158 app.ctx.draw_text(app.width / 2, 20, 'Ball: ${speed_ms:.1f} m/s',
159 size: 20
160 color: color_foreground
161 align: .center
162 )
163
164 // Draw scores and positions
165 // Moved up to be less obtrusive
166 app.ctx.draw_text(app.width / 4, 30, app.p1_score.str(),
167 size: 40
168 color: color_accent
169 align: .center
170 )
171 app.ctx.draw_text(app.width / 4, 80, 'Y: ${int(app.p1_y)}',
172 size: 16
173 color: color_secondary
174 align: .center
175 )
176
177 app.ctx.draw_text(3 * app.width / 4, 30, app.p2_score.str(),
178 size: 40
179 color: color_accent
180 align: .center
181 )
182 app.ctx.draw_text(3 * app.width / 4, 80, 'Y: ${int(app.p2_y)}',
183 size: 16
184 color: color_secondary
185 align: .center
186 )
187
188 if app.paused {
189 app.ctx.draw_text(app.width / 2, app.height / 2 - 50, 'PAUSED',
190 size: 64
191 color: color_accent
192 align: .center
193 )
194 app.ctx.draw_text(app.width / 2, app.height / 2 + 10, 'Press SPACE to Resume',
195 size: 20
196 color: color_accent
197 align: .center
198 )
199 } else {
200 app.ctx.draw_text(app.width / 2, app.height - 25,
201 'SPACE: Pause | W/S: P1 | UP/DOWN: P2 | R: Reset',
202 size: 16
203 color: color_text_dim
204 align: .center
205 )
206 }
207
208 app.ctx.end()
209}
210
211fn on_event(e &gg.Event, data voidptr) {
212 mut app := unsafe { &App(data) }
213 if e.typ == .resized || e.typ == .restored {
214 app.update_sizes()
215 }
216 if e.typ == .key_down {
217 match e.key_code {
218 .space { app.paused = !app.paused }
219 .r { app.reset_game() }
220 else {}
221 }
222 }
223}
224
225fn on_update(dt f32, data voidptr) {
226 mut app := unsafe { &App(data) }
227
228 if app.paused {
229 return
230 }
231
232 scale_y := f32(app.height) / 600.0
233 paddle_speed := paddle_speed_base * scale_y
234
235 // Paddle 1 movement (W/S)
236 if app.ctx.pressed_keys[gg.KeyCode.w] {
237 app.p1_y -= paddle_speed * dt
238 }
239 if app.ctx.pressed_keys[gg.KeyCode.s] {
240 app.p1_y += paddle_speed * dt
241 }
242
243 // Paddle 2 movement (Up/Down)
244 if app.ctx.pressed_keys[gg.KeyCode.up] {
245 app.p2_y -= paddle_speed * dt
246 }
247 if app.ctx.pressed_keys[gg.KeyCode.down] {
248 app.p2_y += paddle_speed * dt
249 }
250
251 // Constrain paddles
252 if app.p1_y < 0 {
253 app.p1_y = 0
254 }
255 if app.p1_y > f32(app.height) - app.paddle_height {
256 app.p1_y = f32(app.height) - app.paddle_height
257 }
258 if app.p2_y < 0 {
259 app.p2_y = 0
260 }
261 if app.p2_y > f32(app.height) - app.paddle_height {
262 app.p2_y = f32(app.height) - app.paddle_height
263 }
264
265 // Ball movement
266 app.ball_x += app.ball_vx * dt
267 app.ball_y += app.ball_vy * dt
268
269 // Ball wall collision (Top/Bottom)
270 if app.ball_y <= 0 {
271 app.ball_y = 0
272 app.ball_vy = -app.ball_vy
273 audio.push(app.sounds.ping.data, app.sounds.ping.len)
274 } else if app.ball_y >= f32(app.height) - app.ball_size {
275 app.ball_y = f32(app.height) - app.ball_size
276 app.ball_vy = -app.ball_vy
277 audio.push(app.sounds.ping.data, app.sounds.ping.len)
278 }
279
280 // Ball paddle collision
281 // P1
282 if app.ball_vx < 0 && app.ball_x <= 20 + app.paddle_width && app.ball_x >= 20 {
283 if app.ball_y + app.ball_size >= app.p1_y && app.ball_y <= app.p1_y + app.paddle_height {
284 app.ball_x = 20 + app.paddle_width
285 app.ball_vx = -app.ball_vx * 1.05 // Slightly speed up
286 // Add some vertical velocity based on where it hit the paddle
287 hit_pos := (app.ball_y + app.ball_size / 2) - (app.p1_y + app.paddle_height / 2)
288 app.ball_vy += hit_pos * 5
289 audio.push(app.sounds.pong.data, app.sounds.pong.len)
290 }
291 }
292
293 // P2
294 if app.ball_vx > 0 && app.ball_x + app.ball_size >= f32(app.width) - 20 - app.paddle_width
295 && app.ball_x + app.ball_size <= f32(app.width) - 20 {
296 if app.ball_y + app.ball_size >= app.p2_y && app.ball_y <= app.p2_y + app.paddle_height {
297 app.ball_x = f32(app.width) - 20 - app.paddle_width - app.ball_size
298 app.ball_vx = -app.ball_vx * 1.05 // Slightly speed up
299 // Add some vertical velocity based on where it hit the paddle
300 hit_pos := (app.ball_y + app.ball_size / 2) - (app.p2_y + app.paddle_height / 2)
301 app.ball_vy += hit_pos * 5
302 audio.push(app.sounds.pong.data, app.sounds.pong.len)
303 }
304 }
305
306 // Score
307 if app.ball_x < 0 {
308 app.p2_score++
309 app.reset_ball()
310 audio.push(app.sounds.buzz.data, app.sounds.buzz.len)
311 } else if app.ball_x > f32(app.width) {
312 app.p1_score++
313 app.reset_ball()
314 audio.push(app.sounds.buzz.data, app.sounds.buzz.len)
315 }
316}
317
318fn main() {
319 mut app := &App{}
320 app.width = 800
321 app.height = 600
322 app.paddle_width = 15.0
323 app.paddle_height = 80.0
324 app.ball_size = 15.0
325 app.p1_y = f32(app.height) / 2 - app.paddle_height / 2
326 app.p2_y = f32(app.height) / 2 - app.paddle_height / 2
327 app.reset_ball()
328
329 audio.setup(buffer_frames: 512)
330 app.sounds.init()
331
332 app.ctx = gg.new_context(
333 width: app.width
334 height: app.height
335 window_title: 'V Pong'
336 user_data: app
337 frame_fn: on_frame
338 event_fn: on_event
339 update_fn: on_update
340 bg_color: color_bg
341 resizable: true
342 create_window: true
343 )
344
345 app.ctx.run()
346}
347