Skip to content

Instantly share code, notes, and snippets.

@Hammer2900
Created December 31, 2025 21:17
Show Gist options
  • Select an option

  • Save Hammer2900/ddf7959cf43fe57530f484c4526af54e to your computer and use it in GitHub Desktop.

Select an option

Save Hammer2900/ddf7959cf43fe57530f484c4526af54e to your computer and use it in GitHub Desktop.
tetris ecs odin
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