diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4351204 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +build: + odin run . -out:space_invaders.exe -debug + +clean: + rm -rf ./space_invaders.exe + +run: + space_invaders.exe diff --git a/assets/texture_atlas.png b/assets/texture_atlas.png new file mode 100644 index 0000000..c7dfd1f Binary files /dev/null and b/assets/texture_atlas.png differ diff --git a/game.odin b/game.odin index 02bec03..bbd2e78 100644 --- a/game.odin +++ b/game.odin @@ -7,25 +7,45 @@ import glm "core:math/linalg/glsl" import rl "vendor:raylib" GLOBAL_SPRITE_SCALE :: 2 +SPRITE_CELL :: 16 ALIEN_ROWS :: 5 ALIENS_PER_ROW :: 11 ALIENS :: ALIEN_ROWS * ALIENS_PER_ROW ALIEN_SIDE_STEP :: 20 * GLOBAL_SPRITE_SCALE -ALIEN_RECT :: glm.ivec2{16 * GLOBAL_SPRITE_SCALE, 16 * GLOBAL_SPRITE_SCALE} +ALIEN_RECT :: glm.vec2{SPRITE_CELL * GLOBAL_SPRITE_SCALE, SPRITE_CELL * GLOBAL_SPRITE_SCALE} MAX_BULLETS :: 100 BULLET_SPEED :: 240 -BULLET_RECT :: glm.ivec2{16 * GLOBAL_SPRITE_SCALE, 16 * GLOBAL_SPRITE_SCALE} +BULLET_RECT :: glm.vec2{SPRITE_CELL * GLOBAL_SPRITE_SCALE, SPRITE_CELL * GLOBAL_SPRITE_SCALE} MAX_PLAYER_HEALTH :: 3 -PLAYER_SPEED :: 40 -PLAYER_RECT :: glm.ivec2{16 * GLOBAL_SPRITE_SCALE, 16 * GLOBAL_SPRITE_SCALE} +PLAYER_SPEED :: 120 +PLAYER_RECT :: glm.vec2{SPRITE_CELL * GLOBAL_SPRITE_SCALE, SPRITE_CELL * GLOBAL_SPRITE_SCALE} + +// texture offset for ship +SHIP_TO :: glm.vec2{0, 112} +BULLET_TO :: glm.vec2{32, 112} + +AlienKind :: enum { + ORANGE, + GREEN, + YELLOW, + RED, +} + +// texture atlas offset for aliens +ALIENS_TO := [AlienKind]glm.vec2 { + .ORANGE = {0, 128 - (SPRITE_CELL * 2)}, + .GREEN = {SPRITE_CELL * 1, 128 - (SPRITE_CELL * 2)}, + .YELLOW = {SPRITE_CELL * 2, 128 - (SPRITE_CELL * 2)}, + .RED = {SPRITE_CELL * 3, 128 - (SPRITE_CELL * 2)}, +} Alien :: struct { alive: bool, position: glm.vec2, - id: int, + id: AlienKind, } Bullet :: struct { @@ -33,26 +53,50 @@ Bullet :: struct { position: glm.vec2, } +collideAABB :: proc( + a_rect: glm.vec2, + a_pos: ^glm.vec2, + b_rect: glm.vec2, + b_pos: ^glm.vec2, +) -> bool { + return( + a_pos.x < b_pos.x + f32(b_rect.x / 2) && + a_pos.x + f32(a_rect.x / 2) > b_pos.x && + a_pos.y < b_pos.y + f32(b_rect.y / 2) && + a_pos.y + f32(a_rect.y / 2) > b_pos.y \ + ) +} + setup_game :: proc(state: ^GameState) { using state - player_pos = glm.vec2{f32(screen_width / 2), f32(screen_height - PLAYER_RECT.x)} + player_pos = glm.vec2{f32(screen_width / 2), f32(screen_height) - PLAYER_RECT.x} // setup the initial positions of the aliens for row in 0 ..< ALIEN_ROWS { for alien in 0 ..< ALIENS_PER_ROW { - aliens[(row * ALIENS_PER_ROW) + alien].position = glm.vec2 { + alien_ptr := &aliens[(row * ALIENS_PER_ROW) + alien] + alien_ptr.position = glm.vec2 { f32((alien + 1) * int(ALIEN_RECT.x + 10)), f32((row + 1) * int(ALIEN_RECT.x + 10)), } + alien_ptr.alive = true } } + // aliens[ALIENS - 1].alive = true + + if player_score > player_high_score { + player_high_score = player_score + } + player_score = 0 + + reset_game = false } update_game :: proc(state: ^GameState) { using state // If we're entering the game screen then we need to setup initial state of the game variables - if last_frame_screen != .GAMEPLAY { + if last_frame_screen != .GAMEPLAY || reset_game { setup_game(state) log.info("Done setting up game") } @@ -66,10 +110,10 @@ update_game :: proc(state: ^GameState) { // Press space to change to fire if (rl.IsKeyPressed(rl.KeyboardKey.SPACE)) { log.info("FIRE!") - bullet := &bullets[bullet_index] - bullet.alive = true - bullet.position = player_pos - bullet.position.y = bullet.position.y - f32((PLAYER_RECT.y / 2) + BULLET_RECT.y / 2) + bullet := &bullets[bullet_index];bullet_index = (bullet_index + 1) % MAX_BULLETS + bullet.alive = true + bullet.position = player_pos + bullet.position.y = bullet.position.y - ((PLAYER_RECT.y / 2) + BULLET_RECT.y / 2.0) } if (rl.IsKeyDown(rl.KeyboardKey.RIGHT)) { @@ -79,39 +123,115 @@ update_game :: proc(state: ^GameState) { player_pos.x = player_pos.x - f32(PLAYER_SPEED * delta_time) } - // movement update - { - for &bullet in bullets { - if bullet.alive { - bullet.position.y = bullet.position.y - f32(BULLET_SPEED * delta_time) - } - } - } -} + all_aliens_dead := true + for &alien in aliens { + if !alien.alive {continue} + all_aliens_dead = false + } -draw_game :: proc(state: ^GameState) { - using state - rl.DrawRectangle(0, 0, state.screen_width, state.screen_height, rl.BLACK) - // rl.DrawText("GAMEPLAY SCREEN", 20, 20, 40, rl.MAROON) - // rl.DrawText("PRESS ENTER or TAP to JUMP to ENDING SCREEN", 130, 220, 20, rl.MAROON) - rl.DrawCircle(c.int(player_pos.x), c.int(player_pos.y), f32(PLAYER_RECT.x / 2), rl.RED) + if all_aliens_dead { + screen = .ENDING + } - for row in 0 ..< ALIEN_ROWS { - for alien in 0 ..< ALIENS_PER_ROW { - alien := &aliens[(row * ALIENS_PER_ROW) + alien] - rl.DrawCircle( - c.int(alien.position.x), - c.int(alien.position.y), - f32(ALIEN_RECT.x / 2), - rl.YELLOW, - ) + // movement update + for &bullet in bullets { + if !bullet.alive {continue} + + bullet.position.y = bullet.position.y - f32(BULLET_SPEED * delta_time) + } + + for &bullet in bullets { + if !bullet.alive {continue} + for &alien in aliens { + if !alien.alive {continue} + if collideAABB(ALIEN_RECT, &alien.position, BULLET_RECT, &bullet.position) { + bullet.alive = false + alien.alive = false + player_score += 10 + log.info("HIT 'EM HARD") + } } } - for &bullet in bullets { - if !bullet.alive { continue } - rl.DrawCircle(c.int(bullet.position.x), c.int(bullet.position.y), f32(BULLET_RECT.x / 2), rl.WHITE) - } +} + +ship_sprite_cell_offset := rl.Rectangle{SHIP_TO.x, SHIP_TO.y, SPRITE_CELL, SPRITE_CELL} +bullet_sprite_cell_offset := rl.Rectangle{BULLET_TO.x, BULLET_TO.y, SPRITE_CELL, SPRITE_CELL} + +draw_game :: proc(state: ^GameState) { + using state + + rl.DrawRectangle(0, 0, state.screen_width, state.screen_height, rl.BLACK) + + rl.DrawTexturePro( + texture_atlas, + ship_sprite_cell_offset, + {player_pos.x, player_pos.y, f32(PLAYER_RECT.x), f32(PLAYER_RECT.y)}, + {SPRITE_CELL, SPRITE_CELL}, + 0, + rl.WHITE, + ) + + for alien in aliens { + if !alien.alive {continue} + rl.DrawTexturePro( + texture_atlas, + {ALIENS_TO[alien.id].x, ALIENS_TO[alien.id].y, SPRITE_CELL, SPRITE_CELL}, + {alien.position.x, alien.position.y, SPRITE_CELL, SPRITE_CELL}, + {SPRITE_CELL, SPRITE_CELL}, + 0, + rl.WHITE, + ) + // rl.DrawCircle( + // c.int(alien.position.x), + // c.int(alien.position.y), + // f32(ALIEN_RECT.x / 2), + // rl.YELLOW, + // ) + } + + for &bullet in bullets { + if !bullet.alive {continue} + rl.DrawCircle( + c.int(bullet.position.x), + c.int(bullet.position.y), + f32(BULLET_RECT.x / 2), + rl.WHITE, + ) + } + + if DEBUG_MODE { + for &bullet in bullets { + if !bullet.alive {continue} + rl.DrawRectangleLines( + c.int(bullet.position.x - (BULLET_RECT.x / 2)), + c.int(bullet.position.y - (BULLET_RECT.y / 2)), + c.int(BULLET_RECT.x), + c.int(BULLET_RECT.y), + rl.YELLOW, + ) + } + for &alien in aliens { + if !alien.alive {continue} + rl.DrawRectangleLines( + c.int(alien.position.x - (ALIEN_RECT.x / 2)), + c.int(alien.position.y - (ALIEN_RECT.y / 2)), + c.int(ALIEN_RECT.x), + c.int(ALIEN_RECT.y), + rl.YELLOW, + ) + } + + rl.DrawRectangleLines( + c.int(player_pos.x - (PLAYER_RECT.x / 2)), + c.int(player_pos.y - (PLAYER_RECT.y / 2)), + c.int(PLAYER_RECT.x), + c.int(PLAYER_RECT.y), + rl.YELLOW, + ) + } + + rl.DrawText(rl.TextFormat("Score: %d", player_score), 130, 220, 20, rl.MAROON) // rl.DrawText("GAMEPLAY SCREEN", 20, 20, 40, rl.MAROON) } diff --git a/main.odin b/main.odin index d567319..0d9c7fd 100644 --- a/main.odin +++ b/main.odin @@ -9,6 +9,11 @@ import rl "vendor:raylib" // orignal resolution of space invaders // 256 x 224 px +TEXTURE_ATLAS_PATH :: "./assets/texture_atlas.png" + +DEBUG_MODE :: true + + GameState :: struct { // window target_fps: c.int, @@ -24,15 +29,20 @@ GameState :: struct { screen: GameScreen, previous_screen: GameScreen, last_frame_screen: GameScreen, + reset_game: bool, aliens: #soa[ALIENS]Alien, bullets: #soa[MAX_BULLETS]Bullet, bullet_index: int, player_pos: glm.vec2, player_health: c.int, player_score: c.int, + player_high_score: c.int, } state: GameState +texture_atlas_image : rl.Image +texture_atlas : rl.Texture2D + setup :: proc(state: ^GameState) { using state target_fps = 60 @@ -76,6 +86,12 @@ main :: proc() { rl.InitWindow(state.screen_width, state.screen_height, state.title) defer rl.CloseWindow() + texture_atlas_image = rl.LoadImage(TEXTURE_ATLAS_PATH) + texture_atlas = rl.LoadTextureFromImage(texture_atlas_image) + rl.UnloadImage(texture_atlas_image) + log.info("Loaded images") + + for !rl.WindowShouldClose() { update(&state) draw(&state) diff --git a/ols.json b/ols.json new file mode 100644 index 0000000..f3c1930 --- /dev/null +++ b/ols.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json", + "collections": [ + { "name": "dependencies", "path": "../dependencies/" } + ], + "enable_semantic_tokens": true, + "enable_document_symbols": true, + "enable_hover": true, + "enable_snippets": true +} diff --git a/screens.odin b/screens.odin index d1894e7..09faa81 100644 --- a/screens.odin +++ b/screens.odin @@ -12,26 +12,27 @@ GameScreen :: enum { update_screen :: proc(state: ^GameState) { - using state + using state switch screen { case .LOGO: { // Wait for 2 seconds (120 frames) before jumping to TITLE screen if (frame_counter > int(target_fps * 2)) { - previous_screen = screen + previous_screen = screen screen = .TITLE log.info("Updated screen enum", screen) } } case .TITLE: { - // Press enter to change to GAMEPLAY screen + // Press enter to change to GAMEPLAY screen if (rl.IsKeyPressed(rl.KeyboardKey.ENTER)) { - previous_screen = screen - screen = .GAMEPLAY + previous_screen = screen + screen = .GAMEPLAY log.info("Updated screen enum", screen) } + reset_game = true } case .GAMEPLAY: { @@ -39,19 +40,19 @@ update_screen :: proc(state: ^GameState) { } case .ENDING: { - // Press enter to return to TITLE screen + // Press enter to return to TITLE screen if (rl.IsKeyPressed(rl.KeyboardKey.ENTER)) { - previous_screen = screen - screen = .TITLE + previous_screen = screen + screen = .TITLE log.info("Updated screen enum", screen) } } } - if last_frame_screen != screen { - log.infof("Current screen: %v, previous screen: %v", screen, previous_screen) - } - last_frame_screen = screen + if last_frame_screen != screen { + log.infof("Current screen: %v, previous screen: %v", screen, previous_screen) + } + last_frame_screen = screen } draw_screen :: proc(state: ^GameState) { @@ -84,18 +85,16 @@ draw_screen :: proc(state: ^GameState) { } case .ENDING: { - // TODO: Draw ENDING screen here! - rl.DrawRectangle(0, 0, state.screen_width, state.screen_height, rl.BLUE) - rl.DrawText("ENDING SCREEN", 20, 20, 40, rl.DARKBLUE) - rl.DrawText( - "PRESS ENTER or TAP to RETURN to TITLE SCREEN", - 120, - 220, - 20, - rl.DARKBLUE, - ) - + draw_ending_screen(state) } } } } + +draw_ending_screen :: proc(state: ^GameState) { +using state + // TODO: Draw ENDING screen here! + rl.DrawRectangle(0, 0, state.screen_width, state.screen_height, rl.BLACK) + rl.DrawText("Game End", 20, 20, 40, rl.WHITE) + rl.DrawText(rl.TextFormat("Player score: %d\nHighscore: %d", player_score, player_high_score), 120, 220, 20, rl.WHITE) +}