diff --git a/assets/particles/merge.png b/assets/particles/merge.png new file mode 100644 index 0000000..3db2674 Binary files /dev/null and b/assets/particles/merge.png differ diff --git a/game.odin b/game.odin index 5ef3198..7e41036 100644 --- a/game.odin +++ b/game.odin @@ -35,6 +35,7 @@ Tile :: struct { value: u16, new_value: u16, merged: bool, + emitter: ParticleEmitter, } Game :: struct { @@ -55,6 +56,7 @@ Game :: struct { help_dialog: bool, icon_textures: [Icon]zephr.TextureId, + merge_particle_texture: zephr.TextureId, palette: ColorPalette, } @@ -72,9 +74,8 @@ neighbors := [4]zephr.Vec2 { {0, -1}, // up {0, 1}, // down } - +@private gameover_anim_time: time.Duration = 0 - @private game : Game @@ -399,6 +400,8 @@ reset_game :: proc() { game.board[y][x].value = 0 game.board[y][x].new_value = 0 game.board[y][x].merged = false + delete(game.board[y][x].emitter.particles) + game.board[y][x].emitter = ParticleEmitter{} } } @@ -418,6 +421,8 @@ game_init :: proc() { game.icon_textures[.SETTINGS_ICON] = zephr.load_texture("assets/icons/settings.png") game.icon_textures[.CLOSE_ICON] = zephr.load_texture("assets/icons/close.png") + game.merge_particle_texture = zephr.load_texture("assets/particles/merge.png") + reset_palette() for i in 0..<2 { @@ -448,14 +453,12 @@ spawn_new_tile_with_value :: proc(x, y: u8, value: u16) { spawn_new_tile :: proc(x, y: u8) { val := get_spawned_tile_value() - game.board[x][y] = Tile{ - value = val, - new_value = val, - position = zephr.Vec2{cast(f32)x, cast(f32)y}, - start_pos = zephr.Vec2{cast(f32)x, cast(f32)y}, - new_pos = zephr.Vec2{cast(f32)x, cast(f32)y}, - scale = zephr.Vec2{1, 1}, - } + game.board[x][y].value = val + game.board[x][y].new_value = val + game.board[x][y].position = zephr.Vec2{cast(f32)x, cast(f32)y} + game.board[x][y].start_pos = zephr.Vec2{cast(f32)x, cast(f32)y} + game.board[x][y].new_pos = zephr.Vec2{cast(f32)x, cast(f32)y} + game.board[x][y].scale = zephr.Vec2{1, 1} } get_random_available_tile_coords :: proc() -> zephr.Vec2 { @@ -558,6 +561,7 @@ game_loop :: proc() { last_frame = now update_positions(delta_t) + update_emitters(delta_t) draw_bg() draw_board() @@ -567,6 +571,14 @@ game_loop :: proc() { } } +update_emitters :: proc(delta_t: time.Duration) { + for row in &game.board { + for tile in &row { + update_emitter(delta_t, &tile.emitter) + } + } +} + update_positions :: proc(delta_t: time.Duration) { if (game.animating) { switch game.last_move_dir { @@ -578,13 +590,14 @@ update_positions :: proc(delta_t: time.Duration) { continue } if src.anim_timer < TILE_ANIM_DURATION { - src.position.x = math.lerp(src.start_pos.x, src.new_pos.x, cast(f32)zephr.ease_in_out_back(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) - src.position.y = math.lerp(src.start_pos.y, src.new_pos.y, cast(f32)zephr.ease_in_out_back(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) + src.position.x = math.lerp(src.start_pos.x, src.new_pos.x, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) + src.position.y = math.lerp(src.start_pos.y, src.new_pos.y, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) src.anim_timer += delta_t } else { dest := &game.board[cast(int)src.new_pos.x][cast(int)src.new_pos.y] if dest.new_value == src.value * 2 { + emitter_start(&dest.emitter, 500, dest.new_pos, time.Millisecond * 200) dest.merged = false } @@ -596,6 +609,7 @@ update_positions :: proc(delta_t: time.Duration) { new_pos = src.new_pos, anim_timer = 0, scale = zephr.Vec2{1, 1}, + emitter = dest.emitter, } src^ = Tile{ value = src.new_value, @@ -605,6 +619,7 @@ update_positions :: proc(delta_t: time.Duration) { new_pos = src.start_pos, anim_timer = 0, scale = zephr.Vec2{1, 1}, + emitter = src.emitter } game.spawning_new_tile = true @@ -619,13 +634,14 @@ update_positions :: proc(delta_t: time.Duration) { continue } if src.anim_timer < TILE_ANIM_DURATION { - src.position.x = math.lerp(src.start_pos.x, src.new_pos.x, cast(f32)zephr.ease_in_out_back(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) - src.position.y = math.lerp(src.start_pos.y, src.new_pos.y, cast(f32)zephr.ease_in_out_back(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) + src.position.x = math.lerp(src.start_pos.x, src.new_pos.x, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) + src.position.y = math.lerp(src.start_pos.y, src.new_pos.y, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) src.anim_timer += delta_t } else { dest := &game.board[cast(int)src.new_pos.x][cast(int)src.new_pos.y] if dest.new_value == src.value * 2 { + emitter_start(&dest.emitter, 500, dest.new_pos, time.Millisecond * 200) dest.merged = false } @@ -637,6 +653,7 @@ update_positions :: proc(delta_t: time.Duration) { new_pos = src.new_pos, anim_timer = 0, scale = zephr.Vec2{1, 1}, + emitter = dest.emitter, } src^ = Tile{ value = src.new_value, @@ -646,6 +663,7 @@ update_positions :: proc(delta_t: time.Duration) { new_pos = src.start_pos, anim_timer = 0, scale = zephr.Vec2{1, 1}, + emitter = src.emitter, } game.spawning_new_tile = true @@ -660,13 +678,14 @@ update_positions :: proc(delta_t: time.Duration) { continue } if src.anim_timer < TILE_ANIM_DURATION { - src.position.x = math.lerp(src.start_pos.x, src.new_pos.x, cast(f32)zephr.ease_in_out_back(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) - src.position.y = math.lerp(src.start_pos.y, src.new_pos.y, cast(f32)zephr.ease_in_out_back(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) + src.position.x = math.lerp(src.start_pos.x, src.new_pos.x, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) + src.position.y = math.lerp(src.start_pos.y, src.new_pos.y, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) src.anim_timer += delta_t } else { dest := &game.board[cast(int)src.new_pos.x][cast(int)src.new_pos.y] if dest.new_value == src.value * 2 { + emitter_start(&dest.emitter, 500, dest.new_pos, time.Millisecond * 200) dest.merged = false } @@ -678,6 +697,7 @@ update_positions :: proc(delta_t: time.Duration) { new_pos = src.new_pos, anim_timer = 0, scale = zephr.Vec2{1, 1}, + emitter = dest.emitter, } src^ = Tile{ value = src.new_value, @@ -687,6 +707,7 @@ update_positions :: proc(delta_t: time.Duration) { new_pos = src.start_pos, anim_timer = 0, scale = zephr.Vec2{1, 1}, + emitter = src.emitter, } game.spawning_new_tile = true @@ -701,13 +722,14 @@ update_positions :: proc(delta_t: time.Duration) { continue } if src.anim_timer < TILE_ANIM_DURATION { - src.position.x = math.lerp(src.start_pos.x, src.new_pos.x, cast(f32)zephr.ease_in_out_back(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) - src.position.y = math.lerp(src.start_pos.y, src.new_pos.y, cast(f32)zephr.ease_in_out_back(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) + src.position.x = math.lerp(src.start_pos.x, src.new_pos.x, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) + src.position.y = math.lerp(src.start_pos.y, src.new_pos.y, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION)) src.anim_timer += delta_t } else { dest := &game.board[cast(int)src.new_pos.x][cast(int)src.new_pos.y] if dest.new_value == src.value * 2 { + emitter_start(&dest.emitter, 500, dest.new_pos, time.Millisecond * 200) dest.merged = false } @@ -719,6 +741,7 @@ update_positions :: proc(delta_t: time.Duration) { new_pos = src.new_pos, anim_timer = 0, scale = zephr.Vec2{1, 1}, + emitter = dest.emitter, } src^ = Tile{ value = src.new_value, @@ -728,6 +751,7 @@ update_positions :: proc(delta_t: time.Duration) { new_pos = src.start_pos, anim_timer = 0, scale = zephr.Vec2{1, 1}, + emitter = src.emitter } game.spawning_new_tile = true @@ -883,6 +907,26 @@ draw_board :: proc() { zephr.set_y_constraint(&tile_con, tile_y_pos, .FIXED) zephr.set_x_constraint(&tile_con, tile_x_pos, .FIXED) + + // Draw particles + particle_con := zephr.DEFAULT_UI_CONSTRAINTS + for particle in tile.emitter.particles { + if (particle.life <= 0) { + continue + } + zephr.set_parent_constraint(&particle_con, &tile_con) + zephr.set_x_constraint(&particle_con, particle.pos.x, .FIXED) + zephr.set_y_constraint(&particle_con, particle.pos.y, .FIXED) + particle_con.scale = particle.size + zephr.set_width_constraint(&particle_con, 30, .RELATIVE_PIXELS) + zephr.set_height_constraint(&particle_con, 1, .ASPECT_RATIO) + zephr.draw_texture(&particle_con, game.merge_particle_texture, zephr.UiStyle{ + bg_color = particle.color, + fg_color = particle.color, + align = .CENTER, + }) + } + zephr.draw_quad(&tile_con, style) // value text diff --git a/particle.odin b/particle.odin new file mode 100644 index 0000000..8c25322 --- /dev/null +++ b/particle.odin @@ -0,0 +1,112 @@ +package main + +import "core:math/rand" +import "core:time" + +import "zephr" + +Particle :: struct { + pos: zephr.Vec2, + vel: zephr.Vec2, + color: zephr.Color, + size: f32, + life: time.Duration, +} + +ParticleEmitter :: struct { + pos: zephr.Vec2, + particles: [dynamic]Particle, + duration: time.Duration, + spawn_rate: u32, +} + +@private +last_emitted_particle_idx: int = 0 +// restricts emission rate to 60fps +@private +particle_time: time.Duration = 0 + +@private +update_particles :: proc(delta_t: time.Duration, particles: ^[dynamic]Particle, start_pos: zephr.Vec2) { + delta_t_f := cast(f32)time.duration_seconds(delta_t) + + for p in particles { + p.life -= delta_t + if p.life <= 0 { + continue + } + + // NOTE: fades faster on higher framerates. don't feel like fixing it + p.color.a -= 1 + p.size -= delta_t_f + p.vel.x += (p.pos.x + p.vel.x / 7) * delta_t_f + p.vel.y -= (p.pos.y + p.vel.y / 7) * delta_t_f + p.pos += p.vel * -delta_t_f + } +} + +@private +find_first_dead_particle :: proc(particles: [dynamic]Particle) -> int { + // search from last emitted particle, should return almost immediately + for i in last_emitted_particle_idx.. 0 { + emitter.duration -= delta_t + if emitter.duration <= 0 { + return + } + + particle_time += delta_t + + for particle_time >= time.Millisecond * 16 { + for i in 0.. TextureId { + context.logger = logger + texture_id: TextureId width, height, channels: i32 data := stb.load(path, &width, &height, &channels, 4) // RGBA + if data == nil { + log.errorf("Failed to load texture: \"%s\"", path) + } defer stb.image_free(data) format := gl.RGBA diff --git a/zephr/ui.odin b/zephr/ui.odin index c5fe41d..147d778 100644 --- a/zephr/ui.odin +++ b/zephr/ui.odin @@ -381,13 +381,13 @@ draw_triangle :: proc(constraints: ^UiConstraints, style: UiStyle) { gl.BindVertexArray(0) } -draw_texture :: proc(constraints: ^UiConstraints, texture_id: TextureId, color: Color, radius: f32, align: Alignment) { +draw_texture :: proc(constraints: ^UiConstraints, texture_id: TextureId, style: UiStyle) { use_shader(ui_shader) - set_vec4f(ui_shader, "aColor", cast(f32)color.r / 255, cast(f32)color.g / 255, cast(f32)color.b / 255, cast(f32)color.a / 255) + set_vec4f(ui_shader, "aColor", cast(f32)style.bg_color.r / 255, cast(f32)style.bg_color.g / 255, cast(f32)style.bg_color.b / 255, cast(f32)style.bg_color.a / 255) set_float(ui_shader, "uiWidth", constraints.width) set_float(ui_shader, "uiHeight", constraints.height) - set_float(ui_shader, "borderRadius", radius) + set_float(ui_shader, "borderRadius", style.border_radius) set_bool(ui_shader, "hasTexture", true) set_mat4f(ui_shader, "projection", zephr_ctx.projection) @@ -395,7 +395,7 @@ draw_texture :: proc(constraints: ^UiConstraints, texture_id: TextureId, color: rect: Rect = --- apply_constraints(constraints, &rect.pos, &rect.size) - apply_alignment(align, constraints, rect.size, &rect.pos) + apply_alignment(style.align, constraints, rect.size, &rect.pos) // set the positions after applying alignment so children can use them constraints.x = rect.pos.x @@ -586,7 +586,11 @@ draw_icon_button :: proc(constraints: ^UiConstraints, icon_tex_id: TextureId, st set_y_constraint(&icon_constraints, 0, .RELATIVE_PIXELS) set_width_constraint(&icon_constraints, constraints.width * 0.8, .FIXED) set_height_constraint(&icon_constraints, constraints.height * 0.8, .FIXED) - draw_texture(&icon_constraints, icon_tex_id, style.fg_color, 0, .CENTER) + tex_style := UiStyle { + bg_color = style.fg_color, + align = .CENTER, + } + draw_texture(&icon_constraints, icon_tex_id, tex_style) if (clicked && state == .ACTIVE) { return true diff --git a/zephr/zephr.odin b/zephr/zephr.odin index 32b4e2f..9077b84 100644 --- a/zephr/zephr.odin +++ b/zephr/zephr.odin @@ -671,6 +671,8 @@ init :: proc(font_path: cstring, icon_path: cstring, window_title: cstring, wind deinit :: proc() { backend_shutdown() + queue.destroy(&zephr_ctx.event_queue) + delete(zephr_ctx.ui.elements) //audio_close() }