From 5978cbcfbf85f12684677630fda4196f2aad5af3 Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Mon, 17 Mar 2025 14:51:33 +0200 Subject: [PATCH] examples: add an asteroids game (it is currently < 400 lines of V, using gg) --- examples/asteroids/.gitignore | 3 + examples/asteroids/asteroids.html | 20 ++ examples/asteroids/asteroids.v | 391 ++++++++++++++++++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 examples/asteroids/.gitignore create mode 100644 examples/asteroids/asteroids.html create mode 100644 examples/asteroids/asteroids.v diff --git a/examples/asteroids/.gitignore b/examples/asteroids/.gitignore new file mode 100644 index 0000000000..350de09ec0 --- /dev/null +++ b/examples/asteroids/.gitignore @@ -0,0 +1,3 @@ +asteroids +asteroids.js +asteroids.wasm diff --git a/examples/asteroids/asteroids.html b/examples/asteroids/asteroids.html new file mode 100644 index 0000000000..d8f84184d9 --- /dev/null +++ b/examples/asteroids/asteroids.html @@ -0,0 +1,20 @@ + + + + + + + V Asteroids + + +

V Asteroids demo

+ + + + + diff --git a/examples/asteroids/asteroids.v b/examples/asteroids/asteroids.v new file mode 100644 index 0000000000..62051f6ac8 --- /dev/null +++ b/examples/asteroids/asteroids.v @@ -0,0 +1,391 @@ +// Copyright (c) 2025 Delyan Angelov. All rights reserved. +// The use of this source code is governed by an MIT license that +// can be found in the LICENSE file. +module main + +import gg +import gx +import math +import rand +import os.asset +import math.vec + +type V2 = vec.Vec2[f32] + +fn rads(degrees f32) f32 { + return f32(math.radians(degrees)) +} + +fn (a V2) + (b V2) V2 { + return a.add(b) +} + +fn (a V2) offset(angle f32, scale f32) V2 { + return a + V2{math.cosf(angle) * scale, math.sinf(angle) * scale} +} + +fn (mut a V2) wrap(b V2) { + a.x = f32(math.mod(a.x + b.x, b.x)) + a.y = f32(math.mod(a.y + b.y, b.y)) +} + +fn V2.random(b V2) V2 { + return V2{rand.f32() * b.x, rand.f32() * b.y} +} + +struct Body { +mut: + pos V2 + vel V2 + rotation f32 + radius f32 + active bool = true +} + +struct Bullet { + Body +} + +struct Asteroid { + Body +mut: + segments int + offsets []f32 + points []f32 + rotation_step f32 = 1.0 +} + +struct Player { + Body +mut: + is_engine_on bool + bullets int = 99 + bullets_limit int = 99 + fuel int = 9999 + fuel_limit int = 9999 + cooldown int + cooldown_frames int = 15 + points []f32 = []f32{len: 8} +} + +struct Game { +mut: + gg &gg.Context = unsafe { nil } + screen V2 = V2{800, 600} + player Player + bullets []Bullet + asteroids []Asteroid + score int + highscore int + ships int = 5 + level int = 1 + is_up bool + + msg Message +} + +@[params] +struct Message { +mut: + frames int + text string + size int = 40 + color gx.Color + align gx.HorizontalAlign = .center +} + +fn (mut a Asteroid) setup() { + a.segments = int(a.radius / 10) * 10 + a.offsets = []f32{len: a.segments} + a.points = []f32{len: 2 * a.segments} + for i in 0 .. a.segments { + a.offsets[i] = a.radius + 25 * (0.5 - rand.f32()) + } +} + +fn (mut p Player) reset(screen V2) { + p.bullets, p.fuel = p.bullets_limit, p.fuel_limit + p.pos, p.vel = screen.div_scalar(2), V2{0, 0} + p.radius, p.rotation = 15, -90 + p.active = true +} + +fn (mut game Game) handle_input() { + mut p := &game.player + p.is_engine_on = false + if game.gg.is_key_down(.escape) { + exit(0) + } + if !p.active { + return + } + game.is_up = game.gg.is_key_down(.up) || game.gg.is_key_down(.w) + is_fire := game.gg.is_key_down(.space) + is_left := game.gg.is_key_down(.left) || game.gg.is_key_down(.a) + is_right := game.gg.is_key_down(.right) || game.gg.is_key_down(.d) + if is_fire && p.cooldown <= 0 && p.active { + game.add_bullet() + p.cooldown = p.cooldown_frames + } + if p.fuel >= 10 { + if game.is_up && p.active { + angle := rads(p.rotation) + p.vel += V2{0, 0}.offset(angle, 0.1) + p.fuel -= 10 + p.is_engine_on = true + } + } + if p.fuel >= 1 { + if is_left { + p.rotation -= 2 + p.fuel-- + } + if is_right { + p.rotation += 2 + p.fuel-- + } + } +} + +fn (mut game Game) update() { + game.msg.frames = int_max(0, game.msg.frames - 1) + mut p := &game.player + p.cooldown = int_max(0, p.cooldown - 1) + p.pos += p.vel + p.pos.wrap(game.screen) + for mut b in game.bullets { + if !b.active { + continue + } + b.pos += b.vel + if b.pos.x < 0 || b.pos.x > game.screen.x || b.pos.y < 0 || b.pos.y > game.screen.y { + b.active = false + } + } + for mut a in game.asteroids { + if !a.active { + continue + } + a.pos += a.vel + a.pos.wrap(game.screen) + a.rotation += a.rotation_step + if p.active && p.pos.distance(a.pos) <= (p.radius + a.radius) { + // player/asteroids collision + p.active = false + a.active = false + game.split_asteroid(a, p.vel.mul_scalar(0.5)) + game.player.reset(game.screen) + game.score += 50 + game.show_message(text: 'Your ship was destroyed.', color: gx.red, frames: 90) + game.ships-- + if game.ships <= 0 { + game.ships = 5 + game.score = 0 + game.asteroids = [] + game.add_asteroids(10) + } + } + } + for mut b in game.bullets { + if !b.active { + continue + } + for mut a in game.asteroids { + if !a.active { + continue + } + if b.active && b.pos.distance(a.pos) <= (b.radius + a.radius) { + // bullet/asteroid collision + b.active = false + a.active = false + game.score += 100 + game.split_asteroid(a, b.vel.mul_scalar(0.2)) + } + } + } + if game.bullets.any(!it.active) { + game.bullets = game.bullets.filter(it.active) + } + if game.asteroids.any(!it.active) { + game.asteroids = game.asteroids.filter(it.active) + } + if game.asteroids.len == 0 { + game.level++ + game.ships++ + game.player.reset(game.screen) + game.add_asteroids(10) + game.show_message(text: 'YOU WIN', color: gx.green, frames: 90) + } + game.highscore = int_max(game.score, game.highscore) +} + +fn (mut game Game) show_message(params Message) { + game.msg = params +} + +fn (mut game Game) split_asteroid(a &Asteroid, vel V2) { + if a.radius < 30 { + return + } + shrink_factor := 0.5 + 0.3 * rand.f32() + mut a1 := Asteroid{ + ...*a + active: true + radius: a.radius * shrink_factor + rotation_step: -a.rotation_step + } + mut a2 := Asteroid{ + ...*a + active: true + radius: a.radius * (1 - shrink_factor) + rotation_step: 2 * a.rotation_step + } + a1.vel = a1.vel.mul_scalar(shrink_factor) * vel + a2.vel = a2.vel.mul_scalar(1 - shrink_factor) * (V2{0, 0} - vel) + a1.setup() + a2.setup() + game.asteroids << a1 + game.asteroids << a2 +} + +fn (mut game Game) draw() { + ws := gg.window_size() + game.screen = V2{ws.width, ws.height} + game.gg.draw_rect_filled(0, 0, game.screen.x, game.screen.y, gx.rgba(20, 20, 20, 255)) + game.draw_ship() + for b in game.bullets { + game.gg.draw_circle_filled(b.pos.x, b.pos.y, 3, gx.yellow) + } + for mut a in game.asteroids { + game.draw_asteroid(mut a) + } + scenter := game.screen.div_scalar(2) + label1 := 'Level: ${game.level} Ships: ${game.ships}' + game.gg.draw_text(5, 10, label1, size: 20, color: gx.white, align: .left) + label2 := 'B: ${game.player.bullets:02} F: ${game.player.fuel:04}' + game.gg.draw_text(int(scenter.x), 10, label2, size: 20, color: gx.green, align: .center) + label3 := 'Score: ${game.score} Highscore: ${game.highscore}' + game.gg.draw_text(int(game.screen.x) - 5, 10, label3, size: 20, color: gx.white, align: .right) + if game.msg.frames > 0 { + game.gg.draw_text(int(scenter.x), int(scenter.y / 2), game.msg.text, + size: game.msg.size + color: game.msg.color + align: game.msg.align + ) + } + label4 := 'Use arrows + space to control your ship. Use Escape to end the game.' + game.gg.draw_text(int(game.screen.x - 5), int(game.screen.y - 20), label4, + size: 16 + color: gx.gray + align: .right + ) +} + +fn (game &Game) draw_ship() { + mut p := &game.player + if !p.active { + return + } + angle := rads(p.rotation) + p1 := p.pos.offset(angle, p.radius) + p2 := p.pos.offset(angle + 2.5, 0.6 * p.radius) + p3 := p.pos.offset(angle, -0.3 * p.radius) + p4 := p.pos.offset(angle - 2.5, 0.6 * p.radius) + p.points[0] = p1.x + p.points[1] = p1.y + p.points[2] = p2.x + p.points[3] = p2.y + p.points[4] = p3.x + p.points[5] = p3.y + p.points[6] = p4.x + p.points[7] = p4.y + game.gg.draw_convex_poly(p.points, gx.white) + if p.is_engine_on { + engine := p.pos.offset(angle + math.pi, 0.7 * p.radius) + game.gg.draw_circle_filled(engine.x, engine.y, 6, gx.yellow) + } +} + +fn (mut game Game) draw_asteroid(mut a Asteroid) { + if !a.active { + return + } + game.gg.draw_circle_filled(a.pos.x, a.pos.y, a.radius, gx.rgba(235, 235, 255, 215)) + for i in 0 .. a.segments { + angle := rads(a.rotation + f32(i) * 360 / a.segments) + p := a.pos.offset(angle, a.offsets[i]) + a.points[i * 2], a.points[i * 2 + 1] = p.x, p.y + } + game.gg.draw_convex_poly(a.points, gx.rgba(155, 155, 148, 245)) +} + +fn (mut game Game) add_bullet() { + if game.player.bullets <= 0 { + return + } + game.player.bullets-- + angle := rads(game.player.rotation) + game.bullets << Bullet{ + pos: game.player.pos + radius: 3 + vel: game.player.vel.offset(angle, 10) + active: true + } +} + +fn (mut game Game) add_asteroids(count int) { + for _ in 0 .. count { + mut npos := V2{} + new_asteroid_loop: for { + npos = V2.random(game.screen) + for a in game.asteroids { + if a.pos.distance(npos) < (a.radius + 30) { + continue new_asteroid_loop + } + } + if game.player.pos.distance(npos) < 5 * (game.player.radius + 30) { + continue new_asteroid_loop + } + break + } + radius := 50 + 50 * (0.5 - rand.f32()) + mut asteroid := Asteroid{ + pos: npos + vel: (V2.random(game.screen) - V2.random(game.screen)) / game.screen + radius: radius + rotation: 360 * rand.f32() + rotation_step: 2 * (0.5 - rand.f32()) + active: true + } + asteroid.setup() + game.asteroids << asteroid + } +} + +fn on_frame(mut game Game) { + if game.gg.timer.elapsed().milliseconds() > 15 { + game.gg.timer.restart() + game.handle_input() + game.update() + } + game.gg.begin() + game.draw() + game.gg.end() +} + +fn main() { + mut game := &Game{} + mut fpath := asset.get_path('../assets', 'fonts/RobotoMono-Regular.ttf') + game.player.reset(game.screen) + game.add_asteroids(10) + game.gg = gg.new_context( + window_title: 'V Asteroids' + width: int(game.screen.x) + height: int(game.screen.y) + frame_fn: on_frame + user_data: game + sample_count: 2 + font_path: fpath + ) + game.gg.run() +}