v / examples / breakout / breakout.v
331 lines · 309 sloc · 8.04 KB · 5d91447d09493d073ae7b8cb1c45a4a00de9222d
Raw
1// vtest build: !openbsd
2import gg
3import math
4import rand
5import sokol.audio
6import os.asset
7import sokol.sgl
8
9const designed_width = 600
10const designed_height = 800
11const brick_width = 53
12const brick_height = 20
13const bevel_size = int(brick_height * 0.18)
14const highlight_color = gg.rgba(255, 255, 255, 65)
15const shade_color = gg.rgba(0, 0, 0, 65)
16
17struct Brick {
18mut:
19 x f32
20 y f32
21 w f32 = brick_width
22 h f32 = brick_height
23 c gg.Color
24 value int
25 alive bool = true
26}
27
28struct Game {
29mut:
30 width int = designed_width
31 height int = designed_height
32 ball_x f32
33 ball_y f32
34 ball_r f32 = 10.0
35 ball_dx f32 = 4
36 ball_dy f32 = -4
37 paddle_x f32 = 250
38 paddle_w f32 = 100
39 paddle_h f32 = 20
40 paddle_dx f32 = 8
41 bricks []Brick
42 nbricks int
43 npaddles int = 10
44 npoints int
45 nlevels int = 1
46 nevent int
47 sound SoundManager
48 ctx &gg.Context = unsafe { nil }
49}
50
51fn Game.new() &Game {
52 mut g := &Game{}
53 g.ball_x, g.ball_y = f32(g.width) / 2, f32(g.height) - g.paddle_h
54 g.init_bricks()
55 return g
56}
57
58enum SoundKind {
59 paddle
60 brick
61 wall
62 lose_ball
63}
64
65struct SoundManager {
66mut:
67 sounds [4][]f32 // TODO: using map[SoundKind][]f32 here breaks emscripten; use map after the fix
68 initialised bool
69}
70
71fn (mut sm SoundManager) init() {
72 all_kinds := [SoundKind.paddle, .brick, .wall, .lose_ball]!
73 sample_rate := f32(audio.sample_rate())
74 duration, volume := 0.09, f32(.25)
75 nframes := int(sample_rate * duration)
76 for i in 0 .. nframes {
77 t := f32(i) / sample_rate
78 sm.sounds[int(SoundKind.paddle)] << volume * math.sinf(t * 936.0 * 2 * math.pi)
79 sm.sounds[int(SoundKind.brick)] << volume * math.sinf(t * 432.0 * 2 * math.pi)
80 sm.sounds[int(SoundKind.wall)] << volume * math.sinf(t * 174.0 * 2 * math.pi)
81 sm.sounds[int(SoundKind.lose_ball)] << math.sinf(t * 123.0 * 2 * math.pi)
82 }
83 border_samples := 2000
84 for k, s := f32(0), 0; s <= border_samples; k, s = k + 1.0 / f32(border_samples), s + 1 {
85 rk := f32(1) - k
86 rs := nframes - border_samples - 1 + s
87 for kind in all_kinds {
88 sm.sounds[int(kind)][s] *= k
89 sm.sounds[int(kind)][rs] *= rk
90 }
91 }
92 sm.initialised = true
93}
94
95fn (mut g Game) play(k SoundKind) {
96 if g.sound.initialised {
97 s := g.sound.sounds[int(k)]
98 audio.push(s.data, s.len)
99 }
100}
101
102fn (mut g Game) init_bricks() {
103 yoffset, xoffset := f32(50 + rand.intn(100) or { 0 }), f32(0 + rand.intn(50) or { 0 })
104 g.bricks.clear()
105 g.nbricks = 0
106 for row in 0 .. 10 {
107 for col in 0 .. 10 {
108 g.bricks << Brick{
109 x: col * (brick_width + 1) + xoffset
110 y: row * (brick_height + 1) + yoffset
111 c: gg.rgb(0x40 | rand.u8(), 0x40 | rand.u8(), 0x40 | rand.u8())
112 value: 10 - row
113 }
114 g.nbricks++
115 }
116 }
117 for _ in 0 .. 5 + rand.intn(10) or { 0 } {
118 i := rand.intn(g.bricks.len - 1) or { 0 }
119 if g.bricks[i].alive {
120 g.bricks[i].alive = false
121 g.nbricks--
122 }
123 }
124}
125
126fn (mut g Game) draw() {
127 ws := gg.window_size()
128 g.ctx.begin()
129 sgl.push_matrix()
130 sgl.scale(f32(ws.width) / f32(designed_width), f32(ws.height) / f32(designed_height), 0)
131
132 g.draw_paddle()
133 g.draw_ball()
134 for brick in g.bricks {
135 if brick.alive {
136 g.draw_brick(brick)
137 }
138 }
139 label1 := 'Level: ${g.nlevels:02} Points: ${g.npoints:06}'
140 label2 := 'Bricks: ${g.nbricks:03} Paddles: ${g.npaddles:02}'
141 g.ctx.draw_text(5, 3, label1, size: 24, color: gg.rgb(255, 255, 255))
142 g.ctx.draw_text(320, 3, label2, size: 24, color: gg.rgb(255, 255, 255))
143
144 sgl.pop_matrix()
145 g.ctx.end()
146}
147
148fn (g &Game) draw_ball() {
149 g.ctx.draw_circle_filled(g.ball_x, g.ball_y, g.ball_r, gg.red)
150 mut ball_r_less := g.ball_r
151 for _ in 0 .. 3 {
152 ball_r_less *= 0.8
153 g.ctx.draw_circle_filled(g.ball_x - g.ball_r + ball_r_less, g.ball_y - g.ball_r +
154 ball_r_less, ball_r_less, highlight_color)
155 }
156}
157
158fn (g &Game) draw_paddle() {
159 roffset, rradius := -5, 18
160 g.ctx.draw_circle_filled(g.paddle_x - roffset, g.height, rradius, gg.blue)
161 g.ctx.draw_circle_filled(g.paddle_x + g.paddle_w + roffset, g.height, rradius, gg.blue)
162 g.ctx.draw_rect_filled(g.paddle_x, g.height - g.paddle_h + 2, g.paddle_w, g.paddle_h, gg.blue)
163 g.ctx.draw_rect_filled(g.paddle_x, g.height - g.paddle_h + 2, g.paddle_w, bevel_size,
164 highlight_color)
165}
166
167fn (g &Game) draw_brick(brick Brick) {
168 g.ctx.draw_rect_filled(brick.x, brick.y, brick.w, brick.h, brick.c)
169 g.ctx.draw_rect_filled(brick.x, brick.y, brick.w, bevel_size, highlight_color)
170 g.ctx.draw_rect_filled(brick.x, brick.y, bevel_size, brick.h - bevel_size, highlight_color)
171 g.ctx.draw_rect_filled(brick.x + brick.w - bevel_size, brick.y, bevel_size,
172 brick.h - bevel_size, shade_color)
173 g.ctx.draw_rect_filled(brick.x, brick.y + brick.h - bevel_size, brick.w, bevel_size,
174 shade_color)
175}
176
177fn (mut g Game) game_over() {
178 g.init_bricks()
179 g.npoints, g.nlevels, g.npaddles = 0, 1, 5
180}
181
182fn (mut g Game) goto_next_level() {
183 g.init_bricks()
184 g.npaddles++
185 g.nlevels++
186}
187
188fn (mut g Game) move(k f32) {
189 if k < 0 {
190 if g.paddle_x <= 0 {
191 return
192 }
193 } else if k > 0 {
194 if g.paddle_x >= g.width - g.paddle_w {
195 return
196 }
197 }
198 g.paddle_x += k * g.paddle_dx
199}
200
201fn (mut g Game) update() {
202 if g.ctx.pressed_keys[gg.KeyCode.left] {
203 g.move(-1.0)
204 }
205 if g.ctx.pressed_keys[gg.KeyCode.right] {
206 g.move(1.0)
207 }
208 //
209 g.ball_x, g.ball_y = g.ball_x + g.ball_dx, g.ball_y + g.ball_dy
210 // Wall collisions
211 if g.ball_x < g.ball_r || g.ball_x > g.width - g.ball_r {
212 g.ball_dx *= -1
213 g.play(.wall)
214 }
215 if g.ball_y < g.ball_r {
216 g.ball_dy *= -1
217 g.play(.wall)
218 }
219 if g.ball_y > g.height {
220 g.ball_x, g.ball_y = g.paddle_x + g.paddle_w / 2, f32(g.height) - g.paddle_h
221 g.ball_dy = -4
222 g.npaddles--
223 g.play(.lose_ball)
224 if g.npaddles <= 0 {
225 g.game_over()
226 }
227 }
228 // Paddle collision
229 is_ball_on_paddle_y := g.ball_y + g.ball_r > g.height - g.paddle_h
230 && g.ball_y < g.height - g.ball_r
231 is_ball_on_paddle_x := g.ball_x > g.paddle_x - 10 && g.ball_x < g.paddle_x + g.paddle_w + 10
232 if is_ball_on_paddle_y && is_ball_on_paddle_x {
233 g.play(.paddle)
234 g.ball_dy = -math.abs(g.ball_dy)
235 x_in_paddle := g.ball_x - g.paddle_x
236 rmargin := 10
237 if x_in_paddle < rmargin || x_in_paddle + rmargin > g.paddle_w {
238 g.ball_dx *= -1
239 } else if !(x_in_paddle > 40 && x_in_paddle < 60) {
240 r := 10 * (-0.5 + rand.f32())
241 g.ball_dx += r
242 g.ball_dx = f32(int_min(int_max(-80, int(g.ball_dx * 10)), 80)) / 10
243 }
244 }
245 // Brick collisions
246 for mut brick in g.bricks {
247 if brick.alive && g.ball_y - g.ball_r < brick.y + brick.h && g.ball_y + g.ball_r > brick.y
248 && g.ball_x + g.ball_r > brick.x && g.ball_x - g.ball_r < brick.x + brick.w {
249 g.play(.brick)
250 brick.alive = false
251 g.nbricks--
252 g.npoints += brick.value
253 g.ball_dy *= -1
254 if g.nbricks == 0 {
255 g.goto_next_level()
256 }
257 }
258 }
259}
260
261fn (mut g Game) touch_event(touch_point gg.TouchPoint) {
262 ws := gg.window_size()
263 tx := touch_point.pos_x
264 if tx <= f32(ws.width) * 0.5 {
265 g.move(-1.0)
266 } else {
267 g.move(1.0)
268 }
269}
270
271@[if wasm32_emscripten]
272fn (mut g Game) handle_event() {
273 if g.nevent > 0 {
274 return
275 }
276 // the audio has to be started when the wasm canvas has received user
277 // interaction, unlike on desktop platforms
278 audio.setup(buffer_frames: 1024)
279 g.sound.init()
280 g.nevent++
281}
282
283fn main() {
284 mut g := Game.new()
285 mut fpath := asset.get_path('../assets', 'fonts/RobotoMono-Regular.ttf')
286 $if !wasm32_emscripten {
287 audio.setup(buffer_frames: 512) // too small values lead to cracking sounds or no sound at all on macos
288 g.sound.init()
289 fpath = ''
290 }
291 g.ctx = gg.new_context(
292 width: g.width
293 height: g.height
294 window_title: 'V Breakout'
295 frame_fn: fn (mut g Game) {
296 dt := g.ctx.timer.elapsed().milliseconds()
297 if dt > 15 {
298 g.update()
299 g.ctx.timer.restart()
300 }
301 g.draw()
302 }
303 click_fn: fn (x f32, y f32, btn gg.MouseButton, mut g Game) {
304 g.handle_event()
305 }
306 event_fn: fn (e &gg.Event, mut g Game) {
307 g.handle_event()
308 if e.typ == .touches_began || e.typ == .touches_moved {
309 if e.num_touches > 0 {
310 touch_point := e.touches[0]
311 g.touch_event(touch_point)
312 }
313 }
314 }
315 keydown_fn: fn (key gg.KeyCode, _ gg.Modifier, mut g Game) {
316 g.handle_event()
317 match key {
318 .r {
319 g.game_over()
320 }
321 .escape {
322 exit(0)
323 }
324 else {}
325 }
326 }
327 user_data: g
328 font_path: fpath
329 )
330 g.ctx.run()
331}
332