Skip to content

Instantly share code, notes, and snippets.

@Hammer2900
Created December 31, 2025 22:53
Show Gist options
  • Select an option

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

Select an option

Save Hammer2900/30f9370de65366d0cd6ed6adfefdfde3 to your computer and use it in GitHub Desktop.
Odin ECS Particle Life Evolution
odin build . -o:speed
package particle_life
import "core:fmt"
import "core:math"
import "core:math/linalg"
import "core:math/rand"
// import "core:slice" // Больше не нужен
import rl "vendor:raylib"
// --- КОНСТАНТЫ ---
SCREEN_W :: 1200
SCREEN_H :: 800
NUM_TYPES :: 6
COLOR_STEP :: 360.0 / f32(NUM_TYPES)
FRICTION :: 0.85
MIN_POPULATION :: 15
NUM_FOOD :: 200
FOOD_RANGE :: 5.0
FOOD_ENERGY :: 100.0
REPRODUCTION_ENERGY :: 1000.0
STARTING_ENERGY :: 400.0
K_FACTOR :: 0.2
PARTICLES_PER_ORG :: 40
// --- КОМПОНЕНТЫ И ДАННЫЕ ---
Genetics :: struct {
internal_forces: [NUM_TYPES][NUM_TYPES]f32,
external_forces: [NUM_TYPES][NUM_TYPES]f32,
internal_mins: [NUM_TYPES][NUM_TYPES]f32,
external_mins: [NUM_TYPES][NUM_TYPES]f32,
internal_radii: [NUM_TYPES][NUM_TYPES]f32,
external_radii: [NUM_TYPES][NUM_TYPES]f32,
}
Particle :: struct {
pos: [2]f32,
vel: [2]f32,
type: int,
}
Organism :: struct {
energy: f32,
genetics: Genetics,
swarm: #soa[dynamic]Particle,
}
Food :: struct {
pos: [2]f32,
}
World :: struct {
organisms: [dynamic]Organism,
food: [dynamic]Food,
}
// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
wrap_vector :: proc(vec: [2]f32) -> [2]f32 {
res := vec
if res.x > SCREEN_W * 0.5 { res.x -= SCREEN_W }
if res.x < SCREEN_W * -0.5 { res.x += SCREEN_W }
if res.y > SCREEN_H * 0.5 { res.y -= SCREEN_H }
if res.y < SCREEN_H * -0.5 { res.y += SCREEN_H }
return res
}
create_random_genetics :: proc() -> Genetics {
g: Genetics
for i in 0..<NUM_TYPES {
for j in 0..<NUM_TYPES {
g.internal_forces[i][j] = rand.float32_range(0.1, 1.0)
g.external_forces[i][j] = rand.float32_range(-1.0, 1.0)
g.internal_mins[i][j] = rand.float32_range(40, 70)
g.external_mins[i][j] = rand.float32_range(40, 70)
g.internal_radii[i][j] = rand.float32_range(g.internal_mins[i][j] * 2, 300)
g.external_radii[i][j] = rand.float32_range(g.external_mins[i][j] * 2, 300)
}
}
return g
}
create_organism :: proc(x, y: f32, genetics: Genetics) -> Organism {
org := Organism{
energy = STARTING_ENERGY,
genetics = genetics,
}
for _ in 0..<PARTICLES_PER_ORG {
px := rand.float32_range(x - 50, x + 50)
py := rand.float32_range(y - 50, y + 50)
pt := rand.int_max(NUM_TYPES)
append(&org.swarm, Particle{
pos = {px, py},
vel = {0, 0},
type = pt,
})
}
return org
}
mutate_genetics :: proc(g: ^Genetics) {
mutate_matrix :: proc(mat: ^[NUM_TYPES][NUM_TYPES]f32, strength, min_v, max_v: f32) {
for i in 0..<NUM_TYPES {
for j in 0..<NUM_TYPES {
if rand.float32() < 0.1 {
mat[i][j] += rand.float32_range(-strength, strength)
}
}
}
}
mutate_matrix(&g.internal_forces, 0.1, 0, 0)
mutate_matrix(&g.external_forces, 0.1, 0, 0)
mutate_matrix(&g.internal_mins, 5.0, 0, 0)
mutate_matrix(&g.external_mins, 5.0, 0, 0)
mutate_matrix(&g.internal_radii, 10.0, 0, 0)
mutate_matrix(&g.external_radii, 10.0, 0, 0)
}
// --- СИСТЕМЫ ECS ---
sys_physics :: proc(world: ^World) {
for i in 0..<len(world.organisms) {
org := &world.organisms[i]
// Получаем длину массива напрямую
count := len(org.swarm)
// 1. Внутренние силы
for p_idx in 0..<count {
total_force: [2]f32
// Прямой доступ к полям SOA (Odin сам оптимизирует это)
p_pos := org.swarm[p_idx].pos
p_type := org.swarm[p_idx].type
for other_idx in 0..<count {
if p_idx == other_idx { continue }
other_pos := org.swarm[other_idx].pos
other_type := org.swarm[other_idx].type
delta := wrap_vector(other_pos - p_pos)
dist := linalg.length(delta)
if dist > 0 {
min_d := org.genetics.internal_mins[p_type][other_type]
max_d := org.genetics.internal_radii[p_type][other_type]
force_val := org.genetics.internal_forces[p_type][other_type]
f: f32 = 0
if dist < min_d {
f = -3.0 * K_FACTOR * abs(force_val) * (1.0 - dist / min_d)
} else if dist < max_d {
f = K_FACTOR * force_val * (1.0 - dist / max_d)
}
total_force += (delta / dist) * f
}
}
// 2. Внешние силы
for j in 0..<len(world.organisms) {
if i == j { continue }
other_org := &world.organisms[j]
other_count := len(other_org.swarm)
for k in 0..<other_count {
ext_pos := other_org.swarm[k].pos
ext_type := other_org.swarm[k].type
delta := wrap_vector(ext_pos - p_pos)
dist := linalg.length(delta)
if dist > 0 {
min_d := org.genetics.external_mins[p_type][ext_type]
max_d := org.genetics.external_radii[p_type][ext_type]
force_val := org.genetics.external_forces[p_type][ext_type]
f: f32 = 0
if dist < min_d {
f = -3.0 * K_FACTOR * abs(force_val) * (1.0 - dist / min_d)
} else if dist < max_d {
f = K_FACTOR * force_val * (1.0 - dist / max_d)
}
total_force += (delta / dist) * f
}
}
}
// Применяем силу (доступ через индекс к SOA работает как ссылка на элемент массива)
org.swarm[p_idx].vel += total_force
}
// 3. Интеграция
for p_idx in 0..<count {
org.swarm[p_idx].vel *= FRICTION
org.swarm[p_idx].pos += org.swarm[p_idx].vel
// Wrap
if org.swarm[p_idx].pos.x < 0 { org.swarm[p_idx].pos.x += SCREEN_W }
if org.swarm[p_idx].pos.x > SCREEN_W { org.swarm[p_idx].pos.x -= SCREEN_W }
if org.swarm[p_idx].pos.y < 0 { org.swarm[p_idx].pos.y += SCREEN_H }
if org.swarm[p_idx].pos.y > SCREEN_H { org.swarm[p_idx].pos.y -= SCREEN_H }
}
org.energy -= 1.0
}
}
sys_eating :: proc(world: ^World) {
for i in 0..<len(world.organisms) {
org := &world.organisms[i]
for p_idx in 0..<len(org.swarm) {
if org.swarm[p_idx].type == 1 {
mouth_pos := org.swarm[p_idx].pos
for f_idx := len(world.food) - 1; f_idx >= 0; f_idx -= 1 {
f := world.food[f_idx]
if linalg.distance(mouth_pos, f.pos) < FOOD_RANGE {
org.energy += FOOD_ENERGY
unordered_remove(&world.food, f_idx)
}
}
}
}
}
}
sys_evolution :: proc(world: ^World) {
for i := len(world.organisms) - 1; i >= 0; i -= 1 {
org := &world.organisms[i]
if org.energy <= 0 {
delete(org.swarm)
unordered_remove(&world.organisms, i)
} else if org.energy > REPRODUCTION_ENERGY {
org.energy -= STARTING_ENERGY
child_genetics := org.genetics
mutate_genetics(&child_genetics)
center: [2]f32 = {0,0}
if len(org.swarm) > 0 { center = org.swarm[0].pos }
child := create_organism(center.x, center.y, child_genetics)
for k in 0..<len(child.swarm) {
if k < len(org.swarm) {
child.swarm[k].type = org.swarm[k].type
}
if rand.float32() < 0.1 {
child.swarm[k].type = rand.int_max(NUM_TYPES)
}
}
append(&world.organisms, child)
}
}
if len(world.organisms) < MIN_POPULATION {
if len(world.organisms) > 0 {
parent := &world.organisms[rand.int_max(len(world.organisms))]
new_gen := parent.genetics
mutate_genetics(&new_gen)
new_org := create_organism(rand.float32_range(0, SCREEN_W), rand.float32_range(0, SCREEN_H), new_gen)
append(&world.organisms, new_org)
} else {
gen := create_random_genetics()
org := create_organism(rand.float32_range(0, SCREEN_W), rand.float32_range(0, SCREEN_H), gen)
append(&world.organisms, org)
}
}
if rand.float32() < 0.1 {
append(&world.food, Food{
pos = {rand.float32_range(0, SCREEN_W), rand.float32_range(0, SCREEN_H)},
})
}
}
sys_draw :: proc(world: ^World) {
rl.BeginDrawing()
rl.ClearBackground(rl.BLACK)
for f in world.food {
rl.DrawCircle(i32(f.pos.x), i32(f.pos.y), 3, rl.GREEN)
}
for org in world.organisms {
for i in 0..<len(org.swarm) {
pos := org.swarm[i].pos
type := org.swarm[i].type
color := rl.ColorFromHSV(f32(type) * COLOR_STEP, 1.0, 1.0)
rl.DrawCircle(i32(pos.x), i32(pos.y), 4, color)
}
}
rl.DrawFPS(10, 10)
rl.DrawText(fmt.ctprintf("Organisms: %d", len(world.organisms)), 10, 30, 20, rl.WHITE)
rl.DrawText(fmt.ctprintf("Food: %d", len(world.food)), 10, 50, 20, rl.WHITE)
rl.EndDrawing()
}
main :: proc() {
rl.InitWindow(SCREEN_W, SCREEN_H, "Odin ECS Particle Life Evolution")
rl.SetTargetFPS(60)
defer rl.CloseWindow()
world := World{}
for _ in 0..<MIN_POPULATION {
gen := create_random_genetics()
org := create_organism(
rand.float32_range(0, SCREEN_W),
rand.float32_range(0, SCREEN_H),
gen,
)
append(&world.organisms, org)
}
for _ in 0..<NUM_FOOD {
append(&world.food, Food{
pos = {rand.float32_range(0, SCREEN_W), rand.float32_range(0, SCREEN_H)},
})
}
for !rl.WindowShouldClose() {
sys_physics(&world)
sys_eating(&world)
sys_evolution(&world)
sys_draw(&world)
}
for org in world.organisms {
delete(org.swarm)
}
delete(world.organisms)
delete(world.food)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment