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