Created
December 31, 2025 21:17
-
-
Save Hammer2900/ddf7959cf43fe57530f484c4526af54e to your computer and use it in GitHub Desktop.
tetris ecs odin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package game | |
| import "core:fmt" | |
| import "core:math" | |
| import "core:math/rand" | |
| import rl "vendor:raylib" | |
| // --- КОНСТАНТЫ --- | |
| CELL_SIZE :: 30 | |
| COLS :: 10 | |
| ROWS :: 20 | |
| SCREEN_W :: 600 | |
| SCREEN_H :: 700 | |
| OFFSET_X :: (SCREEN_W - (COLS * CELL_SIZE)) / 2 | |
| OFFSET_Y :: (SCREEN_H - (ROWS * CELL_SIZE)) / 2 | |
| COLOR_BG :: rl.Color{20, 20, 25, 255} | |
| // --- ТИПЫ И СТРУКТУРЫ --- | |
| PieceType :: enum { I, O, T, S, Z, J, L } | |
| // 1. Компонент Блока (Сущность) | |
| // #soa разложит это на отдельные массивы для скорости | |
| BlockEntity :: struct { | |
| grid_x: int, | |
| grid_y: int, | |
| vis_x: f32, | |
| vis_y: f32, | |
| color: rl.Color, | |
| scale: f32, | |
| is_active: bool, | |
| } | |
| // 2. Мир (Состояние игры) | |
| World :: struct { | |
| blocks: #soa[dynamic]BlockEntity, | |
| // Состояние падающей фигуры | |
| piece_type: PieceType, | |
| piece_pos: [2]int, | |
| piece_vis_pos: [2]f32, | |
| piece_rot: int, | |
| piece_timer: f32, | |
| piece_speed: f32, | |
| // Глобальное состояние | |
| shake_timer: f32, | |
| score: int, | |
| game_over: bool, | |
| } | |
| // Ассеты фигур | |
| SHAPES := [PieceType][][2]int{ | |
| .I = {{-1, 0}, {0, 0}, {1, 0}, {2, 0}}, | |
| .O = {{0, 0}, {1, 0}, {0, 1}, {1, 1}}, | |
| .T = {{-1, 0}, {0, 0}, {1, 0}, {0, 1}}, | |
| .S = {{0, 0}, {1, 0}, {0, 1}, {-1, 1}}, | |
| .Z = {{-1, 0}, {0, 0}, {0, 1}, {1, 1}}, | |
| .J = {{-1, 0}, {0, 0}, {1, 0}, {-1, 1}}, | |
| .L = {{-1, 0}, {0, 0}, {1, 0}, {1, 1}}, | |
| } | |
| COLORS := [PieceType]rl.Color{ | |
| .I = {0, 240, 240, 255}, .O = {240, 240, 0, 255}, .T = {160, 0, 240, 255}, | |
| .S = {0, 240, 0, 255}, .Z = {240, 0, 0, 255}, .J = {0, 0, 240, 255}, | |
| .L = {240, 160, 0, 255}, | |
| } | |
| // --- СИСТЕМЫ (ЛОГИКА) --- | |
| lerp :: proc(start, end, amount: f32) -> f32 { return start + (end - start) * amount } | |
| // Проверка столкновений | |
| sys_check_collision :: proc(world: ^World, px, py, rot: int, ptype: PieceType) -> bool { | |
| shape := SHAPES[ptype] | |
| cos_r := int(math.round(math.cos(f32(rot) * math.PI / 2.0))) | |
| sin_r := int(math.round(math.sin(f32(rot) * math.PI / 2.0))) | |
| for cell in shape { | |
| rx := cell.x * cos_r - cell.y * sin_r | |
| ry := cell.x * sin_r + cell.y * cos_r | |
| x := px + rx | |
| y := py + ry | |
| if x < 0 || x >= COLS || y >= ROWS { return true } | |
| // Проход по ECS массиву | |
| for b in world.blocks { | |
| if b.is_active && b.grid_x == x && b.grid_y == y { | |
| return true | |
| } | |
| } | |
| } | |
| return false | |
| } | |
| // Спавн новой фигуры | |
| sys_spawn_piece :: proc(world: ^World) { | |
| world.piece_type = rand.choice_enum(PieceType) | |
| world.piece_pos = {COLS / 2 - 1, 0} | |
| world.piece_rot = 0 | |
| world.piece_vis_pos = {f32(world.piece_pos.x), f32(world.piece_pos.y)} | |
| if sys_check_collision(world, world.piece_pos.x, world.piece_pos.y, world.piece_rot, world.piece_type) { | |
| world.game_over = true | |
| } | |
| } | |
| // Фиксация фигуры и превращение её в сущности | |
| sys_lock_piece :: proc(world: ^World) { | |
| shape := SHAPES[world.piece_type] | |
| cos_r := int(math.round(math.cos(f32(world.piece_rot) * math.PI / 2.0))) | |
| sin_r := int(math.round(math.sin(f32(world.piece_rot) * math.PI / 2.0))) | |
| color := COLORS[world.piece_type] | |
| for cell in shape { | |
| rx := cell.x * cos_r - cell.y * sin_r | |
| ry := cell.x * sin_r + cell.y * cos_r | |
| final_x := world.piece_pos.x + rx | |
| final_y := world.piece_pos.y + ry | |
| if final_y >= 0 { | |
| append(&world.blocks, BlockEntity{ | |
| grid_x = final_x, | |
| grid_y = final_y, | |
| vis_x = f32(final_x), | |
| vis_y = f32(final_y), | |
| color = color, | |
| scale = 1.5, | |
| is_active = true, | |
| }) | |
| } | |
| } | |
| world.shake_timer = 0.2 | |
| sys_clear_lines(world) | |
| sys_spawn_piece(world) | |
| } | |
| // Удаление линий (ИСПРАВЛЕННАЯ ВЕРСИЯ) | |
| sys_clear_lines :: proc(world: ^World) { | |
| counts: [ROWS]int | |
| for b in world.blocks { | |
| if b.is_active && b.grid_y >= 0 && b.grid_y < ROWS { | |
| counts[b.grid_y] += 1 | |
| } | |
| } | |
| lines_cleared := 0 | |
| for y := 0; y < ROWS; y += 1 { | |
| if counts[y] >= COLS { | |
| lines_cleared += 1 | |
| // Помечаем удаляемые и сдвигаем верхние | |
| for i in 0..<len(world.blocks) { | |
| if world.blocks[i].grid_y == y { | |
| world.blocks[i].is_active = false | |
| } else if world.blocks[i].grid_y < y { | |
| world.blocks[i].grid_y += 1 | |
| } | |
| } | |
| } | |
| } | |
| if lines_cleared > 0 { | |
| world.score += lines_cleared * 100 | |
| // --- БЕЗОПАСНАЯ ОЧИСТКА ПАМЯТИ --- | |
| // Создаем новый массив и переносим туда только живых | |
| // Это работает надежнее всего на Nightly билдах | |
| new_blocks: #soa[dynamic]BlockEntity | |
| for b in world.blocks { | |
| if b.is_active { | |
| append(&new_blocks, b) | |
| } | |
| } | |
| // Удаляем старый, заменяем новым | |
| delete(world.blocks) | |
| world.blocks = new_blocks | |
| // --------------------------------- | |
| } | |
| } | |
| // Анимация | |
| sys_animate :: proc(world: ^World, dt: f32) { | |
| // Фигура | |
| smooth := 15.0 * dt | |
| world.piece_vis_pos.x = lerp(world.piece_vis_pos.x, f32(world.piece_pos.x), smooth) | |
| world.piece_vis_pos.y = lerp(world.piece_vis_pos.y, f32(world.piece_pos.y), smooth) | |
| // Блоки | |
| for i in 0..<len(world.blocks) { | |
| tx := f32(world.blocks[i].grid_x) | |
| ty := f32(world.blocks[i].grid_y) | |
| world.blocks[i].vis_x = lerp(world.blocks[i].vis_x, tx, 10.0 * dt) | |
| world.blocks[i].vis_y = lerp(world.blocks[i].vis_y, ty, 10.0 * dt) | |
| world.blocks[i].scale = lerp(world.blocks[i].scale, 1.0, 8.0 * dt) | |
| } | |
| } | |
| // Рендер | |
| sys_render :: proc(world: ^World, base_x, base_y: f32) { | |
| rl.DrawRectangleLinesEx({base_x - 5, base_y - 5, f32(COLS * CELL_SIZE + 10), f32(ROWS * CELL_SIZE + 10)}, 2, rl.WHITE) | |
| // Статичные блоки | |
| for b in world.blocks { | |
| if !b.is_active { continue } | |
| px := base_x + b.vis_x * CELL_SIZE | |
| py := base_y + b.vis_y * CELL_SIZE | |
| sz := f32(CELL_SIZE) * b.scale | |
| off := (f32(CELL_SIZE) - sz) / 2 | |
| rl.DrawRectangleRec({px + off + 1, py + off + 1, sz - 2, sz - 2}, b.color) | |
| rl.DrawRectangleRec({px + off + 4, py + off + 4, sz/2, sz/2}, {255,255,255,50}) | |
| } | |
| // Падающая фигура | |
| shape := SHAPES[world.piece_type] | |
| cos_r := int(math.round(math.cos(f32(world.piece_rot) * math.PI / 2.0))) | |
| sin_r := int(math.round(math.sin(f32(world.piece_rot) * math.PI / 2.0))) | |
| color := COLORS[world.piece_type] | |
| for cell in shape { | |
| rx := f32(cell.x * cos_r - cell.y * sin_r) | |
| ry := f32(cell.x * sin_r + cell.y * cos_r) | |
| dx := base_x + (world.piece_vis_pos.x + rx) * CELL_SIZE | |
| dy := base_y + (world.piece_vis_pos.y + ry) * CELL_SIZE | |
| rl.DrawRectangleV({dx + 1, dy + 1}, {CELL_SIZE - 2, CELL_SIZE - 2}, color) | |
| rl.DrawRectangleV({dx + 4, dy + 4}, {CELL_SIZE/2, CELL_SIZE/2}, {255,255,255,80}) | |
| } | |
| } | |
| // --- MAIN --- | |
| main :: proc() { | |
| rl.InitWindow(SCREEN_W, SCREEN_H, "Odin ECS Tetris Final") | |
| rl.SetTargetFPS(60) | |
| defer rl.CloseWindow() | |
| world := World{ piece_speed = 0.5 } | |
| sys_spawn_piece(&world) | |
| for !rl.WindowShouldClose() { | |
| dt := rl.GetFrameTime() | |
| if !world.game_over { | |
| // INPUT | |
| new_x, new_rot := world.piece_pos.x, world.piece_rot | |
| moved := false | |
| if rl.IsKeyPressed(.LEFT) { new_x -= 1; moved = true } | |
| if rl.IsKeyPressed(.RIGHT) { new_x += 1; moved = true } | |
| if rl.IsKeyPressed(.UP) { new_rot += 1; moved = true } | |
| if moved { | |
| if !sys_check_collision(&world, new_x, world.piece_pos.y, new_rot, world.piece_type) { | |
| world.piece_pos.x = new_x | |
| world.piece_rot = new_rot | |
| } | |
| } | |
| // GRAVITY | |
| speed_mul: f32 = 1.0 | |
| if rl.IsKeyDown(.DOWN) { speed_mul = 10.0 } | |
| world.piece_timer += dt * speed_mul | |
| if world.piece_timer >= world.piece_speed { | |
| world.piece_timer = 0 | |
| if !sys_check_collision(&world, world.piece_pos.x, world.piece_pos.y + 1, world.piece_rot, world.piece_type) { | |
| world.piece_pos.y += 1 | |
| } else { | |
| sys_lock_piece(&world) | |
| } | |
| } | |
| } | |
| // LOGIC & ANIMATION | |
| sys_animate(&world, dt) | |
| shake_off := rl.Vector2{0, 0} | |
| if world.shake_timer > 0 { | |
| world.shake_timer -= dt | |
| if world.shake_timer < 0 { world.shake_timer = 0 } | |
| if world.shake_timer > 0 { | |
| mag := 5.0 * (world.shake_timer / 0.2) | |
| shake_off.x = rand.float32_range(-mag, mag) | |
| shake_off.y = rand.float32_range(-mag, mag) | |
| } | |
| } | |
| // RENDER | |
| rl.BeginDrawing() | |
| rl.ClearBackground(COLOR_BG) | |
| sys_render(&world, f32(OFFSET_X) + shake_off.x, f32(OFFSET_Y) + shake_off.y) | |
| rl.DrawText(rl.TextFormat("Entities: %d", len(world.blocks)), 10, 10, 20, rl.GREEN) | |
| rl.DrawText(rl.TextFormat("Score: %d", world.score), 10, 35, 20, rl.WHITE) | |
| if world.game_over { | |
| rl.DrawText("GAME OVER", SCREEN_W/2 - 60, SCREEN_H/2, 20, rl.RED) | |
| rl.DrawText("Press R", SCREEN_W/2 - 40, SCREEN_H/2 + 30, 20, rl.GRAY) | |
| if rl.IsKeyPressed(.R) { | |
| delete(world.blocks) // Чистим старый массив | |
| world.blocks = nil // Обнуляем | |
| world.score = 0 | |
| world.game_over = false | |
| sys_spawn_piece(&world) | |
| } | |
| } | |
| rl.EndDrawing() | |
| } | |
| // Очистка при выходе | |
| delete(world.blocks) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment