diff --git a/assets/monogram-extended.ttf b/assets/monogram-extended.ttf new file mode 100644 index 0000000..40f7577 Binary files /dev/null and b/assets/monogram-extended.ttf differ diff --git a/ball.odin b/ball.odin index ccfc951..37c9e58 100644 --- a/ball.odin +++ b/ball.odin @@ -37,13 +37,14 @@ ball_update :: proc(ball: ^Ball, game: ^Game, delta: f32) -> bool { ball.velocity.x = -ball.velocity.x ball.angular_velocity = 0 } + } else { + return false } - if ball.position.y < ball.radius - 20 { + if ball.position.y < ball.radius { ball.position.y = ball.radius ball.velocity.y = -ball.velocity.y ball.angular_velocity = 0 - return false } if ball.position.y + ball.radius > pad.position.y - pad.size.y / 2 && diff --git a/brick.odin b/brick.odin new file mode 100644 index 0000000..1b45062 --- /dev/null +++ b/brick.odin @@ -0,0 +1,44 @@ +package main + +import rl "vendor:raylib" + + + +Brick :: struct { + position: Vec2, + velocity: Vec2, + size: Vec2, + angle: f32, + angular_velocity: f32, + is_flying: bool, + is_fixed: bool, + color: rl.Color, + hits: u8, + is_to_free: bool, +} + +spawn_brick :: proc(position: Vec2, size: Vec2, is_fixed: bool = true, color: rl.Color = rl.RED, hits : u8 = 1) -> Brick { + return Brick { + position = position, + velocity = Vec2{}, + size = Vec2{30,20}, + is_flying = false, + is_fixed = is_fixed, + color = color, + hits = hits, + angle = 0 + } +} + +brick_update :: proc(brick: ^Brick, camera: rl.Camera2D, delta: f32) { + if !brick.is_flying { return } + brick.velocity.y += 500 * delta + brick.position += brick.velocity * delta + brick.angle += brick.angular_velocity * delta + screen_pos := rl.GetWorldToScreen2D(brick.position, camera) + if !rl.CheckCollisionRecs(rl.Rectangle{screen_pos.x - brick.size.x / 2, screen_pos.y - brick.size.y / 2, brick.size.x, brick.size.y}, + rl.Rectangle{0, 0, WINDOWF.x, WINDOWF.y} + ) { + brick.is_to_free = true + } +} diff --git a/game.odin b/game.odin index e1bc7e5..fc0323c 100644 --- a/game.odin +++ b/game.odin @@ -4,53 +4,29 @@ import rl "vendor:raylib" import "vendor:raylib/rlgl" import "core:fmt" import "core:math" +import "core:math/ease" // Virtual game field dimensions GameField := Vec2{800, 600} - -Brick :: struct { - position: Vec2, - velocity: Vec2, - size: Vec2, - angle: f32, - angular_velocity: f32, - is_flying: bool, - is_fixed: bool, - color: rl.Color, - hits: u8, -} - -spawn_brick :: proc(position: Vec2, size: Vec2, is_fixed: bool = true, color: rl.Color = rl.RED, hits : u8 = 1) -> Brick { - return Brick { - position = position, - velocity = Vec2{}, - size = Vec2{30,20}, - is_flying = false, - is_fixed = is_fixed, - color = color, - hits = hits, - angle = 0 - } -} - - Game :: struct { using state: GameState, lives: u8, pad: Pad, balls: [dynamic]Ball, bricks: [dynamic]Brick, - camera: rl.Camera2D + camera: rl.Camera2D, } -game_init :: proc() -> ^GameState { +game_init :: proc(prev: ^GameState = nil) -> ^GameState { state := new(Game) state.variant = state state.draw = game_draw state.update = game_update + state.free = game_free state.lives = 3 + state.previous = prev state.camera = rl.Camera2D{ zoom = 1, target = GameField / 2, @@ -67,6 +43,7 @@ game_init :: proc() -> ^GameState { }) + offset : f32 = 0 for y : f32 = 40; y < GameField.y / 2; y += 35 { for x : f32 = 40 + offset; x < GameField.x - 40; x += 40 { @@ -99,7 +76,10 @@ game_update :: proc(state: ^GameState, delta: f32) { is_inside := ball_update(&ball, game, delta) if is_inside { all_balls_outside = false } - if ball.position.y > GameField.y + 200 { + screen_pos := rl.GetWorldToScreen2D(ball.position, camera) + fmt.println(screen_pos) + if !rl.CheckCollisionCircleRec(ball.position, ball.radius, rl.Rectangle{0, 0, WINDOWF.x, WINDOWF.y} + ) { unordered_remove(&balls, i) } } @@ -115,15 +95,8 @@ game_update :: proc(state: ^GameState, delta: f32) { } #reverse for &brick, i in bricks { - if !brick.is_flying { continue } - brick.velocity.y += 500 * delta - brick.position += brick.velocity * delta - brick.angle += brick.angular_velocity * delta - screen_pos := rl.GetWorldToScreen2D(brick.position, camera) - fmt.println(screen_pos) - if !rl.CheckCollisionRecs(rl.Rectangle{screen_pos.x - brick.size.x / 2, screen_pos.y - brick.size.y / 2, brick.size.x, brick.size.y}, - rl.Rectangle{0, 0, WINDOWF.x, WINDOWF.y} - ) { + brick_update(&brick, camera, delta) + if brick.is_to_free { unordered_remove(&bricks, i) } } @@ -138,7 +111,6 @@ game_draw :: proc(state: ^GameState) { rl.BeginMode2D(camera) - rl.ClearBackground(rl.RAYWHITE) rl.DrawRectangleGradientV(0, 0, i32(GameField.x), i32(GameField.y), rl.RED, rl.RAYWHITE) @@ -154,12 +126,23 @@ game_draw :: proc(state: ^GameState) { } //rl.DrawText(rl.TextFormat("%f\n%f\n%f\n%f", pad.position.x, pad.position.y, pad.velocity.x, pad.velocity.y), 0, 0, 20, rl.BLACK) - rl.DrawText(rl.TextFormat("%d", len(bricks)), 0, 0, 20, rl.BLACK) rl.EndMode2D() for i : u8 = 0; i < lives; i += 1 { rl.DrawCircleV(Vec2{20 + 20 * f32(i), 20}, 10, rl.BLUE) } - + rl.DrawTextEx(FontUI, rl.TextFormat("%d", len(bricks)), {}, f32(48), 2, rl.BLACK) } + + +game_free :: proc(state: ^GameState) { + game := transmute(^Game)state + + delete(game.bricks) + delete(game.balls) + + free(state) +} + + diff --git a/main.odin b/main.odin index 1184396..b7a15c1 100644 --- a/main.odin +++ b/main.odin @@ -1,15 +1,22 @@ package main import rl "vendor:raylib" +import rlgl "vendor:raylib/rlgl" import "core:slice" import "core:fmt" +import "core:math/ease" + Vec2 :: [2]f32 Vec2i :: [2]i32 WINDOW : Vec2i WINDOWF : Vec2 +FontTitle : rl.Font +FontUI: rl.Font + +WindowShouldExit := false main :: proc() { rl.SetConfigFlags(rl.ConfigFlags{.FULLSCREEN_MODE, .VSYNC_HINT, .WINDOW_MAXIMIZED, .WINDOW_UNDECORATED}) @@ -17,6 +24,18 @@ main :: proc() { rl.InitWindow(0, 0, "SinePong") rl.SetTargetFPS(9999) + + tween_init() + defer tween_clean() + + FontUI = rl.LoadFontEx("assets/monogram-extended.ttf", 96, nil, 2048) +// FontUI = rl.LoadFont("assets/monogram-extended.ttf") + FontTitle = rl.LoadFontEx("assets/monogram-extended.ttf", 96*2, nil, 2048) + defer rl.UnloadFont(FontTitle) + defer rl.UnloadFont(FontUI) + rl.SetTextureFilter(FontUI.texture, rl.TextureFilter.POINT) + rl.SetTextureFilter(FontTitle.texture, rl.TextureFilter.POINT) + WINDOW.x = rl.GetScreenWidth() WINDOW.y = rl.GetScreenHeight() WINDOWF = Vec2{f32(WINDOW.x), f32(WINDOW.y)} @@ -24,15 +43,16 @@ main :: proc() { stack_init() defer { - for s, i in state_stack { - free(s) + for state, i in state_stack { + state->free() } } game := game_init() - append(&state_stack, game) + menu := menu_init() + append(&state_stack, menu) - for (!rl.WindowShouldClose()) { + for (!WindowShouldExit) { if rl.IsWindowResized() { WINDOW.x = rl.GetScreenWidth() WINDOW.y = rl.GetScreenHeight() @@ -42,17 +62,17 @@ main :: proc() { current_state := state_stack[len(state_stack)-1] delta := rl.GetFrameTime() + tweens_process(delta) current_state->update(delta) - { rl.BeginDrawing() defer rl.EndDrawing() + rl.ClearBackground(rl.Color{40, 10, 90, 255}) current_state->draw() + rlgl.PopMatrix() } - } - } diff --git a/menu.odin b/menu.odin index 53952df..234f7d0 100644 --- a/menu.odin +++ b/menu.odin @@ -1,8 +1,103 @@ package main import rl "vendor:raylib" +import "core:math/ease" +Menu_Buttons :: enum { + START, + HOW_TO_PLAY, + FULLSCREEN, + EXIT +} + +menu_strings := [Menu_Buttons]cstring { + .START = "Старт", + .HOW_TO_PLAY = "Как играть?", + .FULLSCREEN = "Полный экран", + .EXIT = "Выход" +} + Menu :: struct { using state: GameState, + + menu_pos: Vec2, + menu_element_offset: f32, + active_element: Menu_Buttons, + active_marker: Vec2, + marker_tween: ^Tween, +} + +menu_init :: proc(prev: ^GameState = nil) -> ^GameState { + state := new(Menu) + state.variant = state + state.menu_pos = {300, 300} + state.menu_element_offset = 60 + state.active_marker = state.menu_pos + Vec2{0, f32(state.active_element) * state.menu_element_offset} + state.update = menu_update + state.draw = menu_draw + state.free = menu_free + state.previous = prev + + return state +} + +menu_update :: proc(state: ^GameState, delta: f32) { + menu := transmute(^Menu)state + + prev_element := cast(i8)menu.active_element + cur_element := prev_element + + if rl.IsKeyPressed(rl.KeyboardKey.DOWN) { + cur_element += 1 + } + if rl.IsKeyPressed(rl.KeyboardKey.UP) { + cur_element -= 1 + } + if prev_element != cur_element { + if cur_element < 0 { cur_element = len(Menu_Buttons) -1 } + if cur_element == len(Menu_Buttons) { cur_element = 0 } + menu.active_element = cast(Menu_Buttons)cur_element + if menu.marker_tween != nil { + tween_cancel(menu.marker_tween) + } + menu.marker_tween = tween_to( + &menu.active_marker.y, + menu.menu_pos.y + f32(menu.active_element) * menu.menu_element_offset, + 0.5, + ease.Ease.Back_Out, + ) + } + if rl.IsKeyPressed(rl.KeyboardKey.ENTER) || rl.IsKeyPressed(rl.KeyboardKey.SPACE) { + switch menu.active_element { + case .START: + game := game_init(state) + stack_push(game) + case .FULLSCREEN: + case .HOW_TO_PLAY: + case .EXIT: + WindowShouldExit = true + return + } + } +} + +menu_draw :: proc(state: ^GameState) { + menu := transmute(^Menu)state + + TitleFontSize :: 96 + TitleSpacing :: 3 + TitleText :: "ARKADODGE" + TitleSize := rl.MeasureTextEx(FontTitle, TitleText, TitleFontSize, TitleSpacing) + rl.DrawTextPro(FontTitle, "ARKADODGE", {WINDOWF.x - 50, 50}, {TitleSize.x, 0}, 0, 96, 3, rl.WHITE) + + rl.DrawTextEx(FontUI, ">", menu.active_marker + {-30, 0}, 48, 2, rl.WHITE) + for el, i in menu_strings { + pos := menu.menu_pos + {0, f32(i) * menu.menu_element_offset} + rl.DrawTextEx(FontUI, el, pos, 48, 2, rl.WHITE) + } +} + +menu_free :: proc(state: ^GameState) { + free(state) } diff --git a/pause.odin b/pause.odin new file mode 100644 index 0000000..ad3bef9 --- /dev/null +++ b/pause.odin @@ -0,0 +1,100 @@ +package main + +import rl "vendor:raylib" +import "core:math/ease" + + +Pause_Buttons :: enum { + RESUME, + FULLSCREEN, + EXIT +} + +pause_strings := [Pause_Buttons]cstring { + .RESUME = "Старт", + .FULLSCREEN = "Полный экран", + .EXIT = "Выход" +} + +Pause :: struct { + using state: GameState, + + menu_pos: Vec2, + menu_element_offset: f32, + active_element: Pause_Buttons, + active_marker: Vec2, + marker_tween: ^Tween, +} + +pause_init :: proc(prev: ^GameState = nil) -> ^GameState { + state := new(Pause) + state.variant = state + state.menu_pos = {300, 300} + state.menu_element_offset = 60 + state.active_marker = state.menu_pos + Vec2{0, f32(state.active_element) * state.menu_element_offset} + state.update = menu_update + state.draw = menu_draw + state.free = menu_free + state.previous = prev + + + return state +} + +pause_update :: proc(state: ^GameState, delta: f32) { + menu := transmute(^Pause)state + + prev_element := cast(i8)menu.active_element + cur_element := prev_element + + if rl.IsKeyPressed(rl.KeyboardKey.DOWN) { + cur_element += 1 + } + if rl.IsKeyPressed(rl.KeyboardKey.UP) { + cur_element -= 1 + } + if prev_element != cur_element { + if cur_element < 0 { cur_element = len(Pause_Buttons) -1 } + if cur_element == len(Pause_Buttons) { cur_element = 0 } + menu.active_element = cast(Pause_Buttons)cur_element + if menu.marker_tween != nil { + tween_cancel(menu.marker_tween) + } + menu.marker_tween = tween_to( + &menu.active_marker.y, + menu.menu_pos.y + f32(menu.active_element) * menu.menu_element_offset, + 0.5, + ease.Ease.Back_Out, + ) + } + if rl.IsKeyPressed(rl.KeyboardKey.ENTER) || rl.IsKeyPressed(rl.KeyboardKey.SPACE) { + switch menu.active_element { + case .RESUME: + stack_pop() + case .FULLSCREEN: + case .EXIT: + stack_pop() + stack_pop() + } + } +} + +pause_draw :: proc(state: ^GameState) { + menu := transmute(^Pause)state + + TitleFontSize :: 96 + TitleSpacing :: 3 + TitleText :: "ПАУЗА" + TitleSize := rl.MeasureTextEx(FontTitle, TitleText, TitleFontSize, TitleSpacing) + rl.DrawTextPro(FontTitle, "ARKADODGE", {WINDOWF.x - 50, 50}, {TitleSize.x, 0}, 0, 96, 3, rl.WHITE) + + rl.DrawTextEx(FontUI, ">", menu.active_marker + {-30, 0}, 48, 2, rl.WHITE) + for el, i in menu_strings { + pos := menu.menu_pos + {0, f32(i) * menu.menu_element_offset} + rl.DrawTextEx(FontUI, el, pos, 48, 2, rl.WHITE) + } +} + +pause_free :: proc(state: ^GameState) { + free(state) +} diff --git a/state.odin b/state.odin index 7036481..29cd5b5 100644 --- a/state.odin +++ b/state.odin @@ -2,11 +2,13 @@ package main import "core:slice" -StateVariant :: union{^Game, ^Menu} +StateVariant :: union{^Game, ^Menu, ^Pause} GameState :: struct { update: proc(state: ^GameState, delta: f32), draw: proc(state: ^GameState), + free: proc(state: ^GameState), + previous: ^GameState, variant: StateVariant } @@ -41,6 +43,6 @@ stack_pop :: proc() -> (bool) { return false } state := pop(&state_stack) - free(state) + state->free() return true } diff --git a/tween.odin b/tween.odin new file mode 100644 index 0000000..f0a91a9 --- /dev/null +++ b/tween.odin @@ -0,0 +1,81 @@ +package main + +import "core:math" +import "core:math/ease" +import "core:math/linalg" +import "core:slice" + + +Tween :: struct { + ptr: ^f32, + from: f32, + to: f32, + time: f32, + duration: f32, + ease_type: ease.Ease, + active: bool, + finished: proc(data: rawptr), + data: rawptr +} + + +TWEEN_SIZE :: 128 +tweens_buf: [TWEEN_SIZE]^Tween +tweens : [dynamic]^Tween + +tween_init :: proc() { + tweens = slice.into_dynamic(tweens_buf[:]) +} + +tween_clean :: proc() { + for tween, i in tweens { + free(tween) + } +} + +tween_to :: proc( + value: ^f32, to: f32, duration: f32, + ease: ease.Ease = ease.Ease.Quartic_In_Out, + data: rawptr = nil, + callback: proc(data: rawptr) = nil + ) -> ^Tween { + + tween := new(Tween) + tween.ptr = value + tween.from = value^ + tween.to = to + tween.duration = duration + tween.ease_type = ease + tween.active = true + tween.data = data + tween.finished = callback + + append(&tweens, tween) + return tween +} + +tween_cancel :: proc(t: ^Tween) { + t.active = false +} + +tweens_process :: proc(delta: f32) { + #reverse for tween, i in tweens { + + tween.time += delta + p := clamp(tween.time / tween.duration, 0, 1) + + val := ease.ease(tween.ease_type, p) + tween.ptr^ = math.lerp(tween.from, tween.to, val) + + if tween.time >= tween.duration { + tween.active = false + if tween.finished != nil { + tween.finished(tween.data) + } + } + if !tween.active { + free(tween) + unordered_remove(&tweens, i) + } + } +}