v / examples / asteroids / asteroids.v
386 lines · 360 sloc · 8.79 KB · bbb61ab3687afe512a1fa12492c876d011626107
Raw
1// Copyright (c) 2025 Delyan Angelov. All rights reserved.
2// The use of this source code is governed by an MIT license that
3// can be found in the LICENSE file.
4module main
5
6import gg
7import math
8import rand
9import os.asset
10import math.vec
11
12type V2 = vec.Vec2[f32]
13
14fn rads(degrees f32) f32 {
15 return f32(math.radians(degrees))
16}
17
18fn (a V2) offset(angle f32, scale f32) V2 {
19 return a + V2{math.cosf(angle) * scale, math.sinf(angle) * scale}
20}
21
22fn (mut a V2) wrap(b V2) {
23 a.x = f32(math.mod(a.x + b.x, b.x))
24 a.y = f32(math.mod(a.y + b.y, b.y))
25}
26
27fn V2.random(b V2) V2 {
28 return V2{rand.f32() * b.x, rand.f32() * b.y}
29}
30
31struct Body {
32mut:
33 pos V2
34 vel V2
35 rotation f32
36 radius f32
37 active bool = true
38}
39
40struct Bullet {
41 Body
42}
43
44struct Asteroid {
45 Body
46mut:
47 segments int
48 offsets []f32
49 points []f32
50 rotation_step f32 = 1.0
51}
52
53struct Player {
54 Body
55mut:
56 is_engine_on bool
57 bullets int = 99
58 bullets_limit int = 99
59 fuel int = 9999
60 fuel_limit int = 9999
61 cooldown int
62 cooldown_frames int = 15
63 points []f32 = []f32{len: 8}
64}
65
66struct Game {
67mut:
68 gg &gg.Context = unsafe { nil }
69 screen V2 = V2{800, 600}
70 player Player
71 bullets []Bullet
72 asteroids []Asteroid
73 score int
74 highscore int
75 ships int = 5
76 level int = 1
77 is_up bool
78
79 msg Message
80}
81
82@[params]
83struct Message {
84mut:
85 frames int
86 text string
87 size int = 40
88 color gg.Color
89 align gg.HorizontalAlign = .center
90}
91
92fn (mut a Asteroid) setup() {
93 a.segments = int(a.radius / 10) * 10
94 a.offsets = []f32{len: a.segments}
95 a.points = []f32{len: 2 * a.segments}
96 for i in 0 .. a.segments {
97 a.offsets[i] = a.radius + 25 * (0.5 - rand.f32())
98 }
99}
100
101fn (mut p Player) reset(screen V2) {
102 p.bullets, p.fuel = p.bullets_limit, p.fuel_limit
103 p.pos, p.vel = screen.div_scalar(2), V2{0, 0}
104 p.radius, p.rotation = 15, -90
105 p.active = true
106}
107
108fn (mut game Game) handle_input() {
109 mut p := &game.player
110 p.is_engine_on = false
111 if game.gg.is_key_down(.escape) {
112 exit(0)
113 }
114 if !p.active {
115 return
116 }
117 game.is_up = game.gg.is_key_down(.up) || game.gg.is_key_down(.w)
118 is_fire := game.gg.is_key_down(.space)
119 is_left := game.gg.is_key_down(.left) || game.gg.is_key_down(.a)
120 is_right := game.gg.is_key_down(.right) || game.gg.is_key_down(.d)
121 if is_fire && p.cooldown <= 0 && p.active {
122 game.add_bullet()
123 p.cooldown = p.cooldown_frames
124 }
125 if p.fuel >= 10 {
126 if game.is_up && p.active {
127 angle := rads(p.rotation)
128 p.vel += V2{0, 0}.offset(angle, 0.1)
129 p.fuel -= 10
130 p.is_engine_on = true
131 }
132 }
133 if p.fuel >= 1 {
134 if is_left {
135 p.rotation -= 2
136 p.fuel--
137 }
138 if is_right {
139 p.rotation += 2
140 p.fuel--
141 }
142 }
143}
144
145fn (mut game Game) update() {
146 game.msg.frames = int_max(0, game.msg.frames - 1)
147 mut p := &game.player
148 p.cooldown = int_max(0, p.cooldown - 1)
149 p.pos += p.vel
150 p.pos.wrap(game.screen)
151 for mut b in game.bullets {
152 if !b.active {
153 continue
154 }
155 b.pos += b.vel
156 if b.pos.x < 0 || b.pos.x > game.screen.x || b.pos.y < 0 || b.pos.y > game.screen.y {
157 b.active = false
158 }
159 }
160 for mut a in game.asteroids {
161 if !a.active {
162 continue
163 }
164 a.pos += a.vel
165 a.pos.wrap(game.screen)
166 a.rotation += a.rotation_step
167 if p.active && p.pos.distance(a.pos) <= (p.radius + a.radius) {
168 // player/asteroids collision
169 p.active = false
170 a.active = false
171 game.split_asteroid(a, p.vel.mul_scalar(0.5))
172 game.player.reset(game.screen)
173 game.score += 50
174 game.show_message(text: 'Your ship was destroyed.', color: gg.red, frames: 90)
175 game.ships--
176 if game.ships <= 0 {
177 game.ships = 5
178 game.score = 0
179 game.asteroids = []
180 game.add_asteroids(10)
181 }
182 }
183 }
184 for mut b in game.bullets {
185 if !b.active {
186 continue
187 }
188 for mut a in game.asteroids {
189 if !a.active {
190 continue
191 }
192 if b.active && b.pos.distance(a.pos) <= (b.radius + a.radius) {
193 // bullet/asteroid collision
194 b.active = false
195 a.active = false
196 game.score += 100
197 game.split_asteroid(a, b.vel.mul_scalar(0.2))
198 }
199 }
200 }
201 if game.bullets.any(!it.active) {
202 game.bullets = game.bullets.filter(it.active)
203 }
204 if game.asteroids.any(!it.active) {
205 game.asteroids = game.asteroids.filter(it.active)
206 }
207 if game.asteroids.len == 0 {
208 game.level++
209 game.ships++
210 game.player.reset(game.screen)
211 game.add_asteroids(10)
212 game.show_message(text: 'YOU WIN', color: gg.green, frames: 90)
213 }
214 game.highscore = int_max(game.score, game.highscore)
215}
216
217fn (mut game Game) show_message(params Message) {
218 game.msg = params
219}
220
221fn (mut game Game) split_asteroid(a &Asteroid, vel V2) {
222 if a.radius < 30 {
223 return
224 }
225 shrink_factor := 0.5 + 0.3 * rand.f32()
226 mut a1 := Asteroid{
227 ...*a
228 active: true
229 radius: a.radius * shrink_factor
230 rotation_step: -a.rotation_step
231 }
232 mut a2 := Asteroid{
233 ...*a
234 active: true
235 radius: a.radius * (1 - shrink_factor)
236 rotation_step: 2 * a.rotation_step
237 }
238 a1.vel = a1.vel.mul_scalar(shrink_factor) * vel
239 a2.vel = a2.vel.mul_scalar(1 - shrink_factor) * (V2{0, 0} - vel)
240 a1.setup()
241 a2.setup()
242 game.asteroids << a1
243 game.asteroids << a2
244}
245
246fn (mut game Game) draw() {
247 ws := gg.window_size()
248 game.screen = V2{ws.width, ws.height}
249 game.gg.draw_rect_filled(0, 0, game.screen.x, game.screen.y, gg.rgba(20, 20, 20, 255))
250 game.draw_ship()
251 for b in game.bullets {
252 game.gg.draw_circle_filled(b.pos.x, b.pos.y, 3, gg.yellow)
253 }
254 for mut a in game.asteroids {
255 game.draw_asteroid(mut a)
256 }
257 scenter := game.screen.div_scalar(2)
258 label1 := 'Level: ${game.level} Ships: ${game.ships}'
259 game.gg.draw_text(5, 10, label1, size: 20, color: gg.white, align: .left)
260 label2 := 'B: ${game.player.bullets:02} F: ${game.player.fuel:04}'
261 game.gg.draw_text(int(scenter.x), 10, label2, size: 20, color: gg.green, align: .center)
262 label3 := 'Score: ${game.score} Highscore: ${game.highscore}'
263 game.gg.draw_text(int(game.screen.x) - 5, 10, label3, size: 20, color: gg.white, align: .right)
264 if game.msg.frames > 0 {
265 game.gg.draw_text(int(scenter.x), int(scenter.y / 2), game.msg.text,
266 size: game.msg.size
267 color: game.msg.color
268 align: game.msg.align
269 )
270 }
271 label4 := 'Use arrows + space to control your ship. Use Escape to end the game.'
272 game.gg.draw_text(int(game.screen.x - 5), int(game.screen.y - 20), label4,
273 size: 16
274 color: gg.gray
275 align: .right
276 )
277}
278
279fn (game &Game) draw_ship() {
280 mut p := &game.player
281 if !p.active {
282 return
283 }
284 angle := rads(p.rotation)
285 p1 := p.pos.offset(angle, p.radius)
286 p2 := p.pos.offset(angle + 2.5, 0.6 * p.radius)
287 p3 := p.pos.offset(angle, -0.3 * p.radius)
288 p4 := p.pos.offset(angle - 2.5, 0.6 * p.radius)
289 p.points[0] = p1.x
290 p.points[1] = p1.y
291 p.points[2] = p2.x
292 p.points[3] = p2.y
293 p.points[4] = p3.x
294 p.points[5] = p3.y
295 p.points[6] = p4.x
296 p.points[7] = p4.y
297 game.gg.draw_convex_poly(p.points, gg.white)
298 if p.is_engine_on {
299 engine := p.pos.offset(angle + math.pi, 0.7 * p.radius)
300 game.gg.draw_circle_filled(engine.x, engine.y, 6, gg.yellow)
301 }
302}
303
304fn (mut game Game) draw_asteroid(mut a Asteroid) {
305 if !a.active {
306 return
307 }
308 game.gg.draw_circle_filled(a.pos.x, a.pos.y, a.radius, gg.rgba(235, 235, 255, 215))
309 for i in 0 .. a.segments {
310 angle := rads(a.rotation + f32(i) * 360 / a.segments)
311 p := a.pos.offset(angle, a.offsets[i])
312 a.points[i * 2], a.points[i * 2 + 1] = p.x, p.y
313 }
314 game.gg.draw_convex_poly(a.points, gg.rgba(155, 155, 148, 245))
315}
316
317fn (mut game Game) add_bullet() {
318 if game.player.bullets <= 0 {
319 return
320 }
321 game.player.bullets--
322 angle := rads(game.player.rotation)
323 game.bullets << Bullet{
324 pos: game.player.pos
325 radius: 3
326 vel: game.player.vel.offset(angle, 10)
327 active: true
328 }
329}
330
331fn (mut game Game) add_asteroids(count int) {
332 for _ in 0 .. count {
333 mut npos := V2{}
334 new_asteroid_loop: for {
335 npos = V2.random(game.screen)
336 for a in game.asteroids {
337 if a.pos.distance(npos) < (a.radius + 30) {
338 continue new_asteroid_loop
339 }
340 }
341 if game.player.pos.distance(npos) < 5 * (game.player.radius + 30) {
342 continue new_asteroid_loop
343 }
344 break
345 }
346 radius := 50 + 50 * (0.5 - rand.f32())
347 mut asteroid := Asteroid{
348 pos: npos
349 vel: (V2.random(game.screen) - V2.random(game.screen)) / game.screen
350 radius: radius
351 rotation: 360 * rand.f32()
352 rotation_step: 2 * (0.5 - rand.f32())
353 active: true
354 }
355 asteroid.setup()
356 game.asteroids << asteroid
357 }
358}
359
360fn on_frame(mut game Game) {
361 if game.gg.timer.elapsed().milliseconds() > 15 {
362 game.gg.timer.restart()
363 game.handle_input()
364 game.update()
365 }
366 game.gg.begin()
367 game.draw()
368 game.gg.end()
369}
370
371fn main() {
372 mut game := &Game{}
373 mut fpath := asset.get_path('../assets', 'fonts/RobotoMono-Regular.ttf')
374 game.player.reset(game.screen)
375 game.add_asteroids(10)
376 game.gg = gg.new_context(
377 window_title: 'V Asteroids'
378 width: int(game.screen.x)
379 height: int(game.screen.y)
380 frame_fn: on_frame
381 user_data: game
382 sample_count: 2
383 font_path: fpath
384 )
385 game.gg.run()
386}
387