diff --git a/benches/ecsgame.luau b/benches/ecsgame.luau new file mode 100755 index 0000000..17c3559 --- /dev/null +++ b/benches/ecsgame.luau @@ -0,0 +1,201 @@ +local jecs = require("@jecs") + +local world = jecs.world() +local Physics = world:entity() +local DamagableByPlayer = world:entity() +local Mob = world:entity() +local Player = world:entity() +local Goblin = world:entity() + +local Position = world:component() :: jecs.Id +local Velocity = world:component() :: jecs.Id +local Acceleration = world:component() :: jecs.Id + +local Health = world:component() :: jecs.Id +local MaxHealth = world:component() :: jecs.Id + +local TouchHurts = world:entity() + + +local function setup_player(entity: jecs.Entity) + jecs.bulk_insert(world, entity, + { + MaxHealth, + Health, + Position, + Acceleration, + Velocity, + Player, + Mob, + DamagableByPlayer, + Physics, + }, + { + 100, + 100, + vector.zero, + vector.zero, + vector.zero, + } + ) +end +local function setup_goblin(entity: jecs.Entity) + jecs.bulk_insert(world, entity, + { + MaxHealth, + Health, + Position, + Acceleration, + Velocity, + Mob, + Goblin, + DamagableByPlayer, + Physics + }, + { + 200, + 200, + vector.zero, + vector.zero, + vector.zero, + } + ) +end + +local function setup_wood_spikes(entity: jecs.Entity) + jecs.bulk_insert(world, entity, + { + Position, + TouchHurts, + }, + { + vector.normalize(vector.create(math.random(), math.random(), math.random())) + } + ) +end + +type EntityKind = "player" | "goblin" | "ogre" | "big_boss_goblin" | "wood_spikes" + +local function exhaustive(x: never?) + error(x) +end + +local function entity_create(kind: EntityKind) + local new_entity = world:entity() + if kind == "player" then + setup_player(new_entity) + elseif kind == "big_boss_goblin" then + setup_goblin(new_entity) + elseif kind == "goblin" then + setup_goblin(new_entity) + elseif kind == "wood_spikes" then + setup_wood_spikes(new_entity) + elseif kind == "ogre" then + setup_goblin(new_entity) + else + exhaustive(kind) + end +end + +for i = 1, 10 do + entity_create("player") +end + +for i = 1, 100 do + entity_create("goblin") +end + +for i = 1, 1000 do + entity_create("wood_spikes") +end + +local it = 0 + +local has_physics = world:query(Position, Velocity, Acceleration):cached() +local touch_hurts_against_mobs = world:query(Position):with(TouchHurts):cached() +local damageable_mobs = world:query(Position, Health):with(DamagableByPlayer):cached() +local players = world:query(Position):with(Player):cached() +local dying_mobs = world:query(Health):cached() + +local function physics(dt: number) + for entity, pos, vel, acc in has_physics do + vel += acc * dt + pos += vel * dt + acc = vector.zero + world:set(entity, Velocity, vel) + world:set(entity, Position, vel) + world:set(entity, Acceleration, vel) + it+=1 + end +end + +local function touch_hurts_mobs() + for _, arch2 in damageable_mobs:archetypes() do + local against_positions = arch2.columns_map[Position] + local against_health = arch2.columns_map[Health] + for row2, health in against_health do + it += 1 + local against_pos = against_positions[row2] + for _, arch1 in touch_hurts_against_mobs:archetypes() do + local entity_positions = arch1.columns_map[Position] + for row, entity_pos in entity_positions do + it += 1 + if vector.magnitude(against_pos - entity_pos) < 5 then + continue + end + health -= 1 + end + end + + against_health[row2] = health + end + end +end + +local function players_damage_mobs() + for _, arch2 in damageable_mobs:archetypes() do + local against_health = arch2.columns_map[Health] + for row2, health in against_health do + it += 1 + for _, arch1 in players:archetypes() do + local entity_positions = arch1.columns_map[Position] + for row, entity_pos in entity_positions do + it += 1 + health -= 1 + end + end + + against_health[row2] = health + end + end +end + +local function mobs_die() + for entity, health in dying_mobs do + it += 1 + if health <= 0 then + world:delete(entity) + end + end +end + +local function update(dt: number, tick: number) + it = 0 + physics(dt) + + touch_hurts_mobs(dt) + + players_damage_mobs(dt) + + mobs_die(dt) + + if (tick % 10)==0 then + entity_create("goblin") + entity_create("player") + end +end + +-- 0.062 seconds for 1000 frames. +for i = 1, 1000 do + update(1/60, i) +end diff --git a/benches/smallgame.luau b/benches/smallgame.luau new file mode 100755 index 0000000..8e9ba33 --- /dev/null +++ b/benches/smallgame.luau @@ -0,0 +1,213 @@ +local gamestate = { + entities = {} :: { Entity }, + entity_top_count = 0, + entity_id_gen = 0 +} +for i = 1, 2048 do + gamestate.entities[i] = {} :: Entity +end + +type EntityKind = "player" | "goblin" | "ogre" | "big_boss_goblin" | "wood_spikes" +type EntityHandle = { + index: number, + id: number +} +type Entity = { + allocated: boolean, + handle: EntityHandle, + kind: EntityKind, + + has_physics: boolean, + damagable_by_player: boolean, + is_mob: boolean, + blocks_mobs: boolean, + + position: vector, + velocity: vector, + acceleration: vector, + hitbox: vector, + hit_cooldown_end_time: number, + health: number, + next_attack_time: number, + sprite_id: number, + current_animation_frame: number, + + update: (Entity) -> (), + draw: (Entity) -> (), + max_health: number, + +} + +local function exhaustive(x: never?) + error(x) +end + +local function entity_delete(entity: Entity) + table.clear(entity::any) +end + +local it = 0 +local it_qualified = 0 +local function setup_player(entity: Entity) + entity.kind = "player" + entity.is_mob = true + entity.damagable_by_player = true + entity.max_health = 100 + entity.health = 100 + entity.has_physics = true + entity.acceleration = vector.zero + entity.velocity = vector.zero + entity.position = vector.zero + + entity.update = function() + entity.health = entity.max_health + entity.acceleration += vector.create( + math.random(1, 5), + math.random(1, 5), + math.random(1, 5) + ) + if entity.health <= 0 then + entity_delete(entity) + end + + for _, against in gamestate.entities do + it += 1 + if not against.allocated then + continue + end + if not against.damagable_by_player then + continue + end + it_qualified += 1 + against.health -= 1 + + end + end +end +local function setup_goblin(entity: Entity) + entity.kind = "goblin" + entity.is_mob = true + entity.damagable_by_player = true + entity.max_health = 200 + entity.health = 200 + entity.has_physics = true + entity.acceleration = vector.zero + entity.velocity = vector.zero + entity.position = vector.zero + + entity.update = function() + entity.acceleration += vector.normalize(vector.create(math.random(), math.random(), math.random())) + if entity.health <= 0 then + entity_delete(entity) + end + end +end + +local function setup_wood_spikes(entity: Entity) + entity.kind = "wood_spikes" + entity.is_mob = false + entity.damagable_by_player = false + entity.position = vector.create( + math.random(1, 5), + math.random(1, 5), + math.random(1, 5) + ) + + entity.update = function() + for _, against in gamestate.entities do + it += 1 + if not against.allocated then + continue + end + if against.is_mob == false then + continue + end + if vector.magnitude(against.position - entity.position) < 5 then + continue + end + + it_qualified += 1 + against.health -= 1 + end + end +end + +local function entity_create(kind: EntityKind): Entity + local new_entity: Entity = nil::any + local new_index = 0 + for index, entity in gamestate.entities do + if entity.allocated then + continue + end + new_entity = entity + new_index = index + break + end + + assert(new_index ~= 0) + gamestate.entity_top_count += 1 + + new_entity.handle = {} :: EntityHandle + new_entity.allocated = true + gamestate.entity_id_gen += 1 + new_entity.handle.id = gamestate.entity_id_gen + new_entity.handle.index = new_index + + gamestate.entities[new_index] = new_entity + + if kind == "player" then + setup_player(new_entity) + elseif kind == "big_boss_goblin" then + setup_goblin(new_entity) + elseif kind == "goblin" then + setup_goblin(new_entity) + elseif kind == "wood_spikes" then + setup_wood_spikes(new_entity) + elseif kind == "ogre" then + setup_goblin(new_entity) + else + exhaustive(kind) + end + + return new_entity +end + +for i = 1, 10 do + entity_create("player") +end + +for i = 1, 100 do + entity_create("goblin") +end + +for i = 1, 1000 do + entity_create("wood_spikes") +end + +local function update(dt: number, tick: number) + it = 0 + it_qualified = 0 + for _, entity in gamestate.entities do + it += 1 + if not entity.allocated then + continue + end + it_qualified += 1 + entity.update(entity) + + if entity.has_physics then + entity.velocity += entity.acceleration * dt + entity.position += entity.velocity * dt + entity.acceleration = vector.zero + end + end + if (tick % 10)==0 then + entity_create("goblin") + entity_create("player") + end +end + +-- 14.839 seconds for 1000 frames +for i = 1, 1000 do + update(1/60, i) +end diff --git a/examples/luau/hooks/traits/isa.luau b/examples/luau/hooks/traits/isa.luau new file mode 100755 index 0000000..71125b1 --- /dev/null +++ b/examples/luau/hooks/traits/isa.luau @@ -0,0 +1,46 @@ +local jecs = require("@jecs") +local world = jecs.world() +local IsA = world:entity() +world:set(IsA, jecs.OnAdd, function(component, id) + local second = jecs.pair_second(world, id) + assert(second ~= component, "circular") + + local is_tag = jecs.is_tag(world, second) + world:added(component, function(entity, _, value) + if is_tag then + world:add(entity, second) + else + world:set(entity, second, value) + end + end) + + world:removed(component, function(entity) + world:remove(entity, second) + end) + + if not is_tag then + world:changed(component, function(entity, _, value) + world:set(entity, second, value) + end) + end + + local r = jecs.record(world, second) :: jecs.Record + local archetype = r.archetype + if not archetype then + return + end + local types = archetype.types + + for _, id in types do + if jecs.is_tag(world, id) then + world:add(component, id) + else + local metadata = world:get(second, id) + if not world:has(component, id) then + world:set(component, id, metadata) + end + end + end + + -- jecs.bulk_insert(world, component, ids, values) +end) diff --git a/perf.svg b/perf.svg new file mode 100755 index 0000000..18dd810 --- /dev/null +++ b/perf.svg @@ -0,0 +1,654 @@ + + + + + + + + + + + + + +Flame Graph +Reset Zoom +Search +ic + + + + + + +
Function: [:0] (86,990 usec, 100.0%); self: 0 usec
+ + + +
+ + +./benches/ecsgame.luau:1 +
Function: [./benches/ecsgame.luau:1] (59,386 usec, 68.3%); self: 900 usec
+ + + +
+ + +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1 +
Function: [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1] (27,604 usec, 31.7%); self: 27,604 usec
+ + + +
+ +update +./benches/ecsgame.luau:117 +
Function: update [./benches/ecsgame.luau:117] (55,932 usec, 64.3%); self: 48,036 usec
+ +update +update +
+ +entity_create +./benches/ecsgame.luau:83 +
Function: entity_create [./benches/ecsgame.luau:83] (1,800 usec, 2.1%); self: 0 usec
+ +e.. +entity_create +
+ +world_new +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2206 +
Function: world_new [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2206] (100 usec, 0.1%); self: 0 usec
+ + +world_new +
+ +require +
Function: require [[C]:0] (654 usec, 0.8%); self: 654 usec
+ + +require +
+ +entity_create +./benches/ecsgame.luau:83 +
Function: entity_create [./benches/ecsgame.luau:83] (4,195 usec, 4.8%); self: 100 usec
+ +entity.. +entity_create +
+ +world_query_iter_next +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1747 +
Function: world_query_iter_next [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1747] (300 usec, 0.3%); self: 300 usec
+ + +world_query_iter_next +
+ +world_set +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2399 +
Function: world_set [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2399] (400 usec, 0.5%); self: 400 usec
+ + +world_set +
+ +world_query_iter_next +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1798 +
Function: world_query_iter_next [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1798] (400 usec, 0.5%); self: 400 usec
+ + +world_query_iter_next +
+ +query_archetypes +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1163 +
Function: query_archetypes [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1163] (100 usec, 0.1%); self: 100 usec
+ + +query_archetypes +
+ +world_delete +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2920 +
Function: world_delete [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2920] (2,201 usec, 2.5%); self: 1,900 usec
+ +wo.. +world_delete +
+ +cached_query_iter +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1667 +
Function: cached_query_iter [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1667] (300 usec, 0.3%); self: 300 usec
+ + +cached_query_iter +
+ +setup_player +./benches/ecsgame.luau:20 +
Function: setup_player [./benches/ecsgame.luau:20] (100 usec, 0.1%); self: 0 usec
+ + +setup_player +
+ +world_entity +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2825 +
Function: world_entity [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2825] (400 usec, 0.5%); self: 100 usec
+ + +world_entity +
+ +setup_goblin +./benches/ecsgame.luau:42 +
Function: setup_goblin [./benches/ecsgame.luau:42] (100 usec, 0.1%); self: 0 usec
+ + +setup_goblin +
+ +setup_wood_spikes +./benches/ecsgame.luau:65 +
Function: setup_wood_spikes [./benches/ecsgame.luau:65] (1,200 usec, 1.4%); self: 200 usec
+ + +setup_wood_spikes +
+ +world_add +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2501 +
Function: world_add [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2501] (100 usec, 0.1%); self: 100 usec
+ + +world_add +
+ +setup_goblin +./benches/ecsgame.luau:42 +
Function: setup_goblin [./benches/ecsgame.luau:42] (2,563 usec, 2.9%); self: 400 usec
+ +se.. +setup_goblin +
+ +world_entity +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2825 +
Function: world_entity [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2825] (102 usec, 0.1%); self: 102 usec
+ + +world_entity +
+ +setup_player +./benches/ecsgame.luau:20 +
Function: setup_player [./benches/ecsgame.luau:20] (1,430 usec, 1.6%); self: 100 usec
+ + +setup_player +
+ +archetype_delete +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1095 +
Function: archetype_delete [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1095] (301 usec, 0.3%); self: 301 usec
+ + +archetype_delete +
+ +ecs_bulk_insert +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080 +
Function: ecs_bulk_insert [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080] (100 usec, 0.1%); self: 100 usec
+ + +ecs_bulk_insert +
+ +entity_index_new_id +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2254 +
Function: entity_index_new_id [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2254] (300 usec, 0.3%); self: 100 usec
+ + +entity_index_new_id +
+ +ecs_bulk_insert +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080 +
Function: ecs_bulk_insert [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080] (100 usec, 0.1%); self: 100 usec
+ + +ecs_bulk_insert +
+ +ecs_bulk_insert +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080 +
Function: ecs_bulk_insert [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080] (1,000 usec, 1.1%); self: 900 usec
+ + +ecs_bulk_insert +
+ +ecs_bulk_insert +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080 +
Function: ecs_bulk_insert [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080] (2,063 usec, 2.4%); self: 2,063 usec
+ +e.. +ecs_bulk_insert +
+ +GC +
Function: GC [GC:0] (100 usec, 0.1%); self: 100 usec
+ + +GC +
+ +ecs_bulk_insert +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080 +
Function: ecs_bulk_insert [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080] (1,230 usec, 1.4%); self: 1,130 usec
+ + +ecs_bulk_insert +
+ +GC +
Function: GC [GC:0] (100 usec, 0.1%); self: 100 usec
+ + +GC +
+ +GC +
Function: GC [GC:0] (200 usec, 0.2%); self: 200 usec
+ + +GC +
+ +concat +
Function: concat [[C]:0] (100 usec, 0.1%); self: 0 usec
+ + +concat +
+ +concat +
Function: concat [[C]:0] (100 usec, 0.1%); self: 0 usec
+ + +concat +
+ +GC +
Function: GC [GC:0] (100 usec, 0.1%); self: 100 usec
+ + +GC +
+ +GC +
Function: GC [GC:0] (100 usec, 0.1%); self: 100 usec
+ + +GC +
+
+
+ diff --git a/perf2.svg b/perf2.svg new file mode 100755 index 0000000..ab81724 --- /dev/null +++ b/perf2.svg @@ -0,0 +1,430 @@ + + + + + + + + + + + + + +Flame Graph +Reset Zoom +Search +ic + + + + + + +
Function: [:0] (33,030,510 usec, 100.0%); self: 0 usec
+ + + +
+ + +./benches/smallgame.luau:1 +
Function: [./benches/smallgame.luau:1] (33,030,510 usec, 100.0%); self: 2,214 usec
+ + + +
+ +entity_create +./benches/smallgame.luau:120 +
Function: entity_create [./benches/smallgame.luau:120] (2,900 usec, 0.0%); self: 2,400 usec
+ + +entity_create +
+ +update +./benches/smallgame.luau:176 +
Function: update [./benches/smallgame.luau:176] (33,025,296 usec, 100.0%); self: 134,690 usec
+ +update +update +
+ + +./benches/smallgame.luau:104 +
Function: [./benches/smallgame.luau:104] (23,826,193 usec, 72.1%); self: 23,826,193 usec
+ + + +
+ +entity_create +./benches/smallgame.luau:120 +
Function: entity_create [./benches/smallgame.luau:120] (16,987 usec, 0.1%); self: 15,984 usec
+ + +entity_create +
+ + +./benches/smallgame.luau:60 +
Function: [./benches/smallgame.luau:60] (9,046,526 usec, 27.4%); self: 9,046,526 usec
+ + + +
+
+
+ diff --git a/tools/__pycache__/svg.cpython-312.pyc b/tools/__pycache__/svg.cpython-312.pyc new file mode 100755 index 0000000..efe7e72 Binary files /dev/null and b/tools/__pycache__/svg.cpython-312.pyc differ