add particles

This commit is contained in:
Rokas Puzonas 2026-07-19 12:44:07 +03:00
parent ae088c5a0e
commit 9320b46f68
3 changed files with 191 additions and 6 deletions

View File

@ -6,6 +6,7 @@ const assert = std.debug.assert;
const Math = @import("math");
const Vec2 = Math.Vec2;
const Range = Math.Range;
const STBImage = @import("stb_image");
const STBTrueType = @import("stb_truetype");
@ -26,6 +27,8 @@ const Prng = std.Random.DefaultPrng;
const max_entities = 2048;
const max_particles = 4096;
gpa: Allocator,
camera_pos: Vec2,
prng: Prng,
@ -39,6 +42,9 @@ eternal_flame_id: EntityId,
minions_buffer: [max_entities]EntityId,
minions: std.ArrayList(EntityId),
particles_buffer: [max_particles]Particle,
particles: std.ArrayList(Particle),
upgrades: Upgrades,
shop: Shop,
@ -118,6 +124,18 @@ const Tilesheet = struct {
}
};
const Particle = struct {
size: Vec2,
pos: Vec2,
rotation: f32,
vel: Vec2,
angular_vel: f32,
color: Color,
created_at: Nanoseconds,
dissapear_at: Nanoseconds,
};
const Animation = struct {
frames: []Gfx.Sprite.Id,
frame_duration: Platform.Nanoseconds,
@ -199,6 +217,7 @@ const Entity = struct {
tree_health: u32 = 0,
tree_visual_health: f32 = 0,
tree_max_health: u32 = 0,
tree_trunk_height: u32 = 0,
station_spawn_tree_cooldown: Nanoseconds = 0,
station_owned_tree: ?EntityId = null,
@ -668,6 +687,9 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
.minions_buffer = undefined,
.minions = .initBuffer(&self.minions_buffer),
.particles_buffer = undefined,
.particles = .initBuffer(&self.particles_buffer),
.terrain_sheet = terrain_sheet,
.terrain_tiles = terrain_tiles,
};
@ -949,7 +971,8 @@ fn spawnTree(self: *App, station_id: EntityId) void {
.tree_max_health = health,
.seed = rng.int(u64),
.pos = station.pos.add(.init((rng.float(f32)-0.5) * terrain_tile_size.x, 0))
.pos = station.pos.add(.init((rng.float(f32)-0.5) * terrain_tile_size.x, 0)),
.tree_trunk_height = rng.uintAtMost(u32, 4) + 2
})) |id| {
station.station_owned_tree = id;
}
@ -1670,6 +1693,7 @@ fn showDebug(self: *App, plt: Platform.Frame) !void {
}
_ = ImGUI.checkbox("Minion state", &self.show_minion_state);
_ = ImGUI.text("Particle count {}", .{self.particles.items.len});
ImGUI.separator();
_ = ImGUI.text("Minion upgrades", .{});
@ -1767,7 +1791,7 @@ fn showDebug(self: *App, plt: Platform.Frame) !void {
}
}
fn drawTree(self: *App, seed: u64, pos: Vec2, health_percent: f32) void {
fn drawTree(self: *App, plt: Platform.Frame, seed: u64, pos: Vec2, trunk_height: u32, health_percent: f32) void {
var prng = Prng.init(seed);
const rng = prng.random();
@ -1789,8 +1813,6 @@ fn drawTree(self: *App, seed: u64, pos: Vec2, health_percent: f32) void {
self.terrain_sheet.get(125, 39),
};
const trunk_height = rng.uintAtMost(u32, 4) + 2;
Gfx.drawSprite(self.terrain_sheet.get(125, 42), .init(0, 0), .init(1, 1), .white);
for (0..trunk_height) |i| {
const sprite = tree_middle[rng.uintLessThan(u32, tree_middle.len)];
@ -1831,6 +1853,19 @@ fn drawTree(self: *App, seed: u64, pos: Vec2, health_percent: f32) void {
Gfx.drawSprite(corner_leaves, .init(2, -1), .init(-1, -1), .white);
}
self.spawnParticles(plt, .{
.pos = pos.add(.init(0, -terrain_tile_size.y*(@as(f32, @floatFromInt(trunk_height))+1.5))),
.size = .init(4, 4),
.radius = .init(10, 40),
.count = .init(0, 0.03),
.duration_s = .init(0.5, 2.5),
.angle = .init(0, std.math.pi),
.color_variance = 0.1,
.color = .rgb(40, 150, 40),
.vel_y = .init(10, 60),
.angular_vel = .init(-4, 4)
});
if (health_percent < 1) {
drawProgressBar(1, .init(0, 20), .init(terrain_tile_size.x, 10), .black);
drawProgressBar(health_percent, .init(0, 20), .init(terrain_tile_size.x, 10), .white);
@ -1859,6 +1894,61 @@ fn drawEntityAnimation(self: *App, e: *Entity, dt: Nanoseconds) void {
}
}
const ParticleOptions = struct {
pos: Vec2,
size: Vec2,
count: Range,
radius: Range,
angle: Range = .init(0, std.math.pi*2),
duration_s: Range = .init(0.5, 1),
vel_x: Range = .init(0, 0),
vel_y: Range = .init(0, 0),
angular_vel: Range = .init(0, 0),
color: Color = .white,
color_variance: f32 = 0
};
fn spawnParticles(self: *App, plt: Platform.Frame, opts: ParticleOptions) void {
const rng = self.prng.random();
var count_f32: f32 = 0;
if (opts.count.to < 1) {
if (opts.count.to > rng.float(f32)) {
count_f32 = 1;
}
} else {
count_f32 = opts.count.random(rng);
}
for (0..@intFromFloat(count_f32)) |_| {
const radius = opts.radius.random(rng);
const angle = opts.angle.random(rng);
const duration_s = opts.duration_s.random(rng);
const color = Color{
.r = opts.color.r + ((rng.float(f32)-0.5)*2) * opts.color_variance,
.g = opts.color.g + ((rng.float(f32)-0.5)*2) * opts.color_variance,
.b = opts.color.b + ((rng.float(f32)-0.5)*2) * opts.color_variance,
.a = opts.color.a + ((rng.float(f32)-0.5)*2) * opts.color_variance,
};
const offset = Vec2.init(radius, 0).rotate(angle);
self.particles.appendBounded(Particle{
.color = color,
.pos = opts.pos.add(offset),
.size = opts.size,
.rotation = 0,
.created_at = plt.t,
.dissapear_at = plt.t + @as(Nanoseconds,@intFromFloat(duration_s * std.time.ns_per_s)),
.vel = .init(opts.vel_x.random(rng), opts.vel_y.random(rng)),
.angular_vel = opts.angular_vel.random(rng)
}) catch {
log.warn("Particle limit reached", .{});
return;
};
}
}
pub fn frame(self: *App, plt: Platform.Frame) !void {
const input = plt.input;
_ = input; // autofix
@ -2168,6 +2258,19 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
break :blk animations.right;
}
};
if (minion.minion_animation == .walk) {
self.spawnParticles(plt, .{
.pos = minion.pos,
.size = .init(8, 8),
.radius = .init(5, 30),
.count = .init(0, 0.03),
.duration_s = .init(0.5, 1.5),
.angle = .init(0, std.math.pi),
.color_variance = 0.1,
.color = .rgb(40, 150, 40),
});
}
}
}
@ -2249,17 +2352,59 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
const health_percent = e.tree_visual_health / @as(f32, @floatFromInt(e.tree_max_health));
self.drawTree(
plt,
e.seed,
e.pos,
e.tree_trunk_height,
health_percent
);
if (e.tree_visual_health < 0.1) {
self.destroyTree(id);
for (0..e.tree_trunk_height) |i| {
self.spawnParticles(plt, .{
.pos = e.pos.add(.init(0, -terrain_tile_size.y*@as(f32,@floatFromInt(i+1)))),
.size = .init(4, 6),
.radius = .init(1, 16),
.count = .init(20, 70),
.duration_s = .init(0.2, 1.0),
.angle = .init(0, std.math.pi*2),
.color_variance = 0.1,
.color = .rgb(200, 120, 40),
.angular_vel = .init(-8, 8)
});
}
}
}
}
{ // Draw particles
var i: usize = 0;
while (i < self.particles.items.len) {
const particle = &self.particles.items[i];
if (plt.t >= particle.dissapear_at) {
_ = self.particles.swapRemove(i);
continue;
}
const duration = @as(f64, @floatFromInt(particle.dissapear_at - particle.created_at));
const progress = @as(f64, @floatFromInt(plt.t - particle.created_at)) / duration;
particle.pos = particle.pos.add(particle.vel.multiplyScalar(dt));
particle.rotation += particle.angular_vel*dt;
var color = particle.color;
color.a *= (1-@as(f32, @floatCast(progress)));
var quad = Gfx.Quad.initRect(particle.size.divideScalar(-2), particle.size);
quad.applyRotate(particle.rotation, .init(0, 0));
quad.applyOffset(particle.pos);
Gfx.drawQuad(quad, color);
i += 1;
}
}
{ // Draw eternal flame
var eternal_flame_iter = self.initEntityIteratorByType(.eternal_flame);
while (eternal_flame_iter.next()) |id| {
@ -2300,25 +2445,52 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
eternal_flame.eternal_flame_size = .large;
}
var particle_radius: f32 = 0;
var particle_count: f32 = 0;
var particle_vel: f32 = 0;
switch (eternal_flame.eternal_flame_size) {
.small => {
const flame_size = Vec2.init(16, 32).multiplyScalar(4);
eternal_flame.animation = self.fire1_animation;
eternal_flame.animation_rect = .init(-flame_size.x/2, -flame_size.y, flame_size.x, flame_size.y);
particle_radius = 40;
particle_count = 0.05;
particle_vel = 70;
},
.medium => {
const flame_size = Vec2.init(32, 48).multiplyScalar(4);
eternal_flame.animation = self.fire2_animation;
eternal_flame.animation_rect = .init(-flame_size.x/2, -flame_size.y, flame_size.x, flame_size.y);
particle_radius = 60;
particle_count = 0.1;
particle_vel = 100;
},
.large => {
const flame_size = Vec2.init(32, 48).multiplyScalar(4);
eternal_flame.animation = self.fire5_animation;
eternal_flame.animation_rect = .init(-flame_size.x/2, -flame_size.y, flame_size.x, flame_size.y);
particle_radius = 80;
particle_count = 0.3;
particle_vel = 120;
}
}
self.drawEntityAnimation(eternal_flame, plt.dt);
self.spawnParticles(plt, .{
.pos = eternal_flame.pos.sub(.init(0, 70)),
.size = .init(4, 12),
.radius = .init(0, particle_radius),
.count = .init(0, particle_count),
.duration_s = .init(0.5, 3),
.color_variance = 0.15,
.color = .rgb(200, 40, 40),
.vel_y = .init(-1, -particle_vel),
.angular_vel = .init(-4, 4)
});
}
}

View File

@ -41,3 +41,12 @@ pub fn rgb_hex(text: []const u8) ?Color {
const b = std.fmt.parseInt(u8, text[5..7], 16) catch return null;
return rgb(r, g, b);
}
pub fn lerp(self: Color, other: Color, t: f32) Color {
return Color{
.r = std.math.lerp(self.r, other.r, t),
.g = std.math.lerp(self.g, other.g, t),
.b = std.math.lerp(self.b, other.b, t),
.a = std.math.lerp(self.a, other.a, t),
};
}

View File

@ -39,7 +39,7 @@ const State = struct {
nearest_sampler: sg.Sampler,
default_sampler: Sampler,
quads_buffer: [2048]VertexQuad,
quads_buffer: [8192]VertexQuad,
quads: std.ArrayList(VertexQuad),
clear_color: Color,
@ -530,9 +530,13 @@ pub fn endFrame() void {
}
pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void {
drawQuad(Quad.initRect(pos, size), color);
}
pub fn drawQuad(quad: Quad, color: Color) void {
const self = &g_state;
drawSprite(self.default_sprite, pos, size, color);
drawSpriteQuad(self.default_sprite, quad, color);
}
pub fn drawRectangleOutline(pos: Vec2, size: Vec2, color: Color, width: f32, alignment: f32) void {