Skip to content

Commit 2dc5f68

Browse files
authored
examples: add a small breakout game, supporting keyboard and touch controls (#23861)
1 parent 9e36d2c commit 2dc5f68

File tree

3 files changed

+324
-0
lines changed

3 files changed

+324
-0
lines changed

examples/breakout/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
breakout
2+
breakout.js
3+
breakout.wasm

examples/breakout/breakout.html

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!doctype html>
2+
<html lang="en-us">
3+
<head>
4+
<meta charset=utf-8>
5+
<meta content="text/html; charset=utf-8" http-equiv=Content-Type>
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>V Breakout</title>
8+
</head>
9+
<body>
10+
<h1>V Breakout demo</h1>
11+
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1" width="600" height="800"></canvas>
12+
<script type='text/javascript'>
13+
var canvasElement = document.getElementById('canvas');
14+
var Module = {
15+
print(...args) { console.log(...args); },
16+
};
17+
</script>
18+
<script src=breakout.js></script>
19+
</body>
20+
</html>

examples/breakout/breakout.v

+301
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
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

Comments
 (0)