diff --git a/benches/visual/query.bench.luau b/benches/visual/query.bench.luau index 6516900..ad6ba4b 100755 --- a/benches/visual/query.bench.luau +++ b/benches/visual/query.bench.luau @@ -2,7 +2,7 @@ --!native local ReplicatedStorage = game:GetService("ReplicatedStorage") -local Matter = require(ReplicatedStorage.DevPackages.matter) +local Matter = require(ReplicatedStorage.DevPackages.Matter) local ecr = require(ReplicatedStorage.DevPackages.ecr) local newWorld = Matter.World.new() diff --git a/demo/src/ReplicatedStorage/components.luau b/demo/src/ReplicatedStorage/components.luau index 5bb3225..b4f0ccb 100755 --- a/demo/src/ReplicatedStorage/components.luau +++ b/demo/src/ReplicatedStorage/components.luau @@ -1,3 +1,5 @@ +--!strict +local RunService = game:GetService("RunService") local ReplicatedStorage = game:GetService("ReplicatedStorage") local jecs = require(ReplicatedStorage.ecs) local types = require("./types") @@ -5,31 +7,58 @@ local types = require("./types") local Networked = jecs.tag() local NetworkedPair = jecs.tag() -local Renderable = jecs.component() :: jecs.Id -jecs.meta(Renderable, Networked) +local InstanceMapping = jecs.component() :: jecs.Id +jecs.meta(InstanceMapping, jecs.OnAdd, function(component) + jecs.meta(component, jecs.OnAdd, function(entity, _, instance) + if RunService:IsServer() then + instance:SetAttribute("entity_server") + else + instance:SetAttribute("entity_client") + end + end) +end) +local function networked_id(ct) + jecs.meta(ct, Networked) + return ct +end +local function networked_pair(ct) + jecs.meta(ct, NetworkedPair) + return ct +end +local function instance_mapping_id(ct) + jecs.meta(ct, InstanceMapping) + return ct +end -local Poison = jecs.component() :: jecs.Id -jecs.meta(Poison, Networked) - -local Health = jecs.component() :: jecs.Id -jecs.meta(Health, Networked) - -local Player = jecs.component() :: jecs.Id -jecs.meta(Player, Networked) - +local Renderable = jecs.component() :: types.Id +local Poison = jecs.component() :: types.Id +local Health = jecs.component() :: types.Id +local Player = jecs.component() :: types.Id +local Debuff = jecs.tag() :: types.Entity +local Lifetime = jecs.component() :: types.Id<{ + duration: number, + created: number +}> +local Destroy = jecs.tag() local components = { - Renderable = Renderable, - Player = Player, - Poison = Poison, - Health = Health, + Renderable = networked_id(instance_mapping_id(Renderable)), + Player = networked_id(Player), + Poison = networked_id(Poison), + Health = networked_id(Health), + Lifetime = networked_id(Lifetime), + Debuff = networked_id(Debuff), + Destroy = networked_id(Destroy), + + -- We have to define that some builtin IDs can also be networked + ChildOf = networked_pair(jecs.ChildOf), Networked = Networked, NetworkedPair = NetworkedPair, } -for name, component in components do +for name, component in components :: {[string]: types.Id } do jecs.meta(component, jecs.Name, name) end diff --git a/demo/src/ReplicatedStorage/main.client.luau b/demo/src/ReplicatedStorage/main.client.luau index 11dffbc..e600f67 100755 --- a/demo/src/ReplicatedStorage/main.client.luau +++ b/demo/src/ReplicatedStorage/main.client.luau @@ -10,4 +10,5 @@ local world = observers_add(jecs.world()) local systems = ReplicatedStorage.systems SYSTEM(world, systems.receive_replication) +SYSTEM(world, systems.entities_delete) RUN(world) diff --git a/demo/src/ReplicatedStorage/systems/entities_delete.luau b/demo/src/ReplicatedStorage/systems/entities_delete.luau new file mode 100755 index 0000000..07e4169 --- /dev/null +++ b/demo/src/ReplicatedStorage/systems/entities_delete.luau @@ -0,0 +1,13 @@ + +--!strict +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local types = require(ReplicatedStorage.types) +local ct = require(ReplicatedStorage.components) + +local function entities_delete(world: types.World, dt: number) + for e in world:each(ct.Destroy) do + world:delete(e) + end +end + +return entities_delete diff --git a/demo/src/ReplicatedStorage/systems/receive_replication.luau b/demo/src/ReplicatedStorage/systems/receive_replication.luau index a28e0b7..febed9c 100755 --- a/demo/src/ReplicatedStorage/systems/receive_replication.luau +++ b/demo/src/ReplicatedStorage/systems/receive_replication.luau @@ -2,45 +2,51 @@ local types = require("../types") local jecs = require(game:GetService("ReplicatedStorage").ecs) local remotes = require("../remotes") local collect = require("../collect") -local client_ids = {} +local components = require("../components") -local function ecs_map_get(world, id) - local deserialised_id = client_ids[id] +local client_ids: {[jecs.Entity]: jecs.Entity } = {} - if not deserialised_id then - if world:has(id, jecs.Name) then - deserialised_id = world:entity(id) - else - deserialised_id = world:entity() +local function ecs_ensure_entity(world: jecs.World, id: jecs.Entity) + local e = 0 + + local ser_id = id + local deser_id = client_ids[ser_id] + if deser_id then + if deser_id == 0 then + local new_id = world:entity() + client_ids[ser_id] = new_id + deser_id = new_id end - - client_ids[id] = deserialised_id + else + if not world:exists(ser_id) + or (world:contains(ser_id) and not world:get(ser_id, jecs.Name)) + then + deser_id = world:entity() + else + if world:contains(ser_id) and world:get(ser_id, jecs.Name) then + deser_id = ser_id + else + deser_id = world:entity() + end + end + client_ids[ser_id] = deser_id end - -- local deserialised_id = client_ids[id] - -- if not deserialised_id then - -- if world:has(id, jecs.Name) then - -- deserialised_id = world:entity(id) - -- else - -- if world:exists(id) then - -- deserialised_id = world:entity() - -- else - -- deserialised_id = world:entity(id) - -- end - -- end - -- client_ids[id] = deserialised_id - -- end + e = deser_id - return deserialised_id + return e end -local function ecs_make_alive_id(world, id) - local rel = jecs.ECS_PAIR_FIRST(id) - local tgt = jecs.ECS_PAIR_SECOND(id) +-- local rel_render = `e{jecs.ECS_ID(rel)}v{jecs.ECS_GENERATION(rel)}` +-- local tgt_render = `e{jecs.ECS_ID(tgt)}v{jecs.ECS_GENERATION(tgt)}` +local function ecs_deser_pairs(world, token) + local tokens = string.split(token, ",") + local rel = tonumber(tokens[1]) + local tgt = tonumber(tokens[2]) - rel = ecs_map_get(world, rel) - tgt = ecs_map_get(world, tgt) + rel = ecs_ensure_entity(world, rel) + tgt = ecs_ensure_entity(world, tgt) return jecs.pair(rel, tgt) end @@ -48,25 +54,31 @@ end local snapshots = collect(remotes.replication.OnClientEvent) return function(world: types.World) + for entity in world:each(components.Destroy) do + client_ids[entity] = nil + end for snapshot in snapshots do - for id, map in snapshot do - id = tonumber(id) - if jecs.IS_PAIR(id) then - id = ecs_make_alive_id(world, id) + for ser_id, map in snapshot do + local id = tonumber(ser_id) + if not id then + id = ecs_deser_pairs(world, ser_id) + else + id = ecs_ensure_entity(world, id) end local set = map.set if set then if jecs.is_tag(world, id) then for _, entity in set do - entity = ecs_map_get(world, entity) + entity = ecs_ensure_entity(world, entity) world:add(entity, id) end else + local t = os.clock() local values = map.values for i, entity in set do - entity = ecs_map_get(world, entity) - world:set(entity, id, values[i]) + entity = ecs_ensure_entity(world, entity) + world:set(entity, id, values[i]) end end end @@ -75,7 +87,7 @@ return function(world: types.World) if removed then for _, entity in removed do - entity = ecs_map_get(world, entity) + entity = ecs_ensure_entity(world, entity) if not world:contains(entity) then continue end diff --git a/demo/src/ServerScriptService/main.server.luau b/demo/src/ServerScriptService/main.server.luau index aa9f0f5..833717d 100755 --- a/demo/src/ServerScriptService/main.server.luau +++ b/demo/src/ServerScriptService/main.server.luau @@ -15,5 +15,8 @@ local systems = ServerScriptService.systems SYSTEM(world, systems.replication) SYSTEM(world, systems.players_added) SYSTEM(world, systems.poison_hurts) +SYSTEM(world, systems.health_regen) +SYSTEM(world, systems.lifetimes_expire) SYSTEM(world, systems.life_is_painful) +SYSTEM(world, ReplicatedStorage.systems.entities_delete) RUN(world, 0) diff --git a/demo/src/ServerScriptService/systems/health_regen.luau b/demo/src/ServerScriptService/systems/health_regen.luau new file mode 100755 index 0000000..db691af --- /dev/null +++ b/demo/src/ServerScriptService/systems/health_regen.luau @@ -0,0 +1,15 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ct = require(ReplicatedStorage.components) +local types = require(ReplicatedStorage.types) + +return function(world: types.World, dt: number) + for e in world:query(ct.Player):without(ct.Health) do + world:set(e, ct.Health, 100) + end + + for e, health in world:query(ct.Health) do + if math.random() < 1 / 60 / 30 then + world:set(e, ct.Health, 100) + end + end +end diff --git a/demo/src/ServerScriptService/systems/life_is_painful.luau b/demo/src/ServerScriptService/systems/life_is_painful.luau index 333e64b..023883b 100755 --- a/demo/src/ServerScriptService/systems/life_is_painful.luau +++ b/demo/src/ServerScriptService/systems/life_is_painful.luau @@ -1,12 +1,19 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local ct = require(ReplicatedStorage.components) local types = require(ReplicatedStorage.types) +local jecs = require(ReplicatedStorage.ecs) return function(world: types.World, dt: number) - for e in world:query(ct.Player):without(ct.Health) do - world:set(e, ct.Health, 100) - end - for e in world:query(ct.Player, ct.Health):without(ct.Poison) do - world:set(e, ct.Poison, 10) + if math.random() < (1 / 60 / 7) then + for e in world:each(ct.Health) do + local poison = world:entity() + world:add(poison, ct.Debuff) + world:add(poison, jecs.pair(jecs.ChildOf, e)) + world:set(poison, ct.Poison, 10) + world:set(poison, ct.Lifetime, { + duration = 3, + created = os.clock() + }) + end end end diff --git a/demo/src/ServerScriptService/systems/lifetimes_expire.luau b/demo/src/ServerScriptService/systems/lifetimes_expire.luau new file mode 100755 index 0000000..7bb808c --- /dev/null +++ b/demo/src/ServerScriptService/systems/lifetimes_expire.luau @@ -0,0 +1,12 @@ + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ct = require(ReplicatedStorage.components) +local types = require(ReplicatedStorage.types) + +return function(world: types.World, dt: number) + for e, lifetime in world:query(ct.Lifetime) do + if os.clock() > lifetime.created + lifetime.duration then + world:add(e, ct.Destroy) + end + end +end diff --git a/demo/src/ServerScriptService/systems/players_added.luau b/demo/src/ServerScriptService/systems/players_added.luau index 46b1ab1..597f5ec 100755 --- a/demo/src/ServerScriptService/systems/players_added.luau +++ b/demo/src/ServerScriptService/systems/players_added.luau @@ -12,9 +12,13 @@ return function(world: types.World, dt: number) for entity, player in world:query(ct.Player):without(ct.Renderable) do local character = player.Character - if character then - if not character.Parent then - world:set(entity, ct.Renderable, character) + if not character then + continue end + if not character.Parent then + continue + end + + world:set(entity, ct.Renderable, character) end end diff --git a/demo/src/ServerScriptService/systems/poison_hurts.luau b/demo/src/ServerScriptService/systems/poison_hurts.luau index a7e1f3e..6991d39 100755 --- a/demo/src/ServerScriptService/systems/poison_hurts.luau +++ b/demo/src/ServerScriptService/systems/poison_hurts.luau @@ -1,12 +1,18 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local ct = require(ReplicatedStorage.components) -return function(world, dt) - for e, poison, health in world:query(ct.Poison, ct.Health) do - local health_after_tick = health - poison * dt * 0.05 - if health_after_tick < 0 then - world:remove(e, ct.Health) +local types = require(ReplicatedStorage.types) +local jecs = require(ReplicatedStorage.ecs) + +return function(world: types.World, dt: number) + for e, poison_tick in world:query(ct.Poison, jecs.pair(jecs.ChildOf, jecs.w)) do + local tgt = world:target(e, jecs.ChildOf) + local health = world:get(tgt, ct.Health) + if not health then continue end - world:set(e, ct.Health, health_after_tick) + + if math.random() < 1 / 60 / 1 and health > 1 then + world:set(tgt, ct.Health, health - 1) + end end end diff --git a/demo/src/ServerScriptService/systems/replication.luau b/demo/src/ServerScriptService/systems/replication.luau index dab7723..20c7d52 100755 --- a/demo/src/ServerScriptService/systems/replication.luau +++ b/demo/src/ServerScriptService/systems/replication.luau @@ -1,9 +1,13 @@ + +--!strict +local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") -local types = require("../../ReplicatedStorage/types") -local ct = require("../../ReplicatedStorage/components") +local ct = require(ReplicatedStorage.components) +local components = ct :: { [string]: jecs.Entity } +local remotes = require(ReplicatedStorage.remotes) local jecs = require(ReplicatedStorage.ecs) -local remotes = require("../../ReplicatedStorage/remotes") -local components = ct :: {[string]: jecs.Entity } +local collect = require(ReplicatedStorage.collect) +local ty = require(ReplicatedStorage.types) return function(world: ty.World) @@ -19,9 +23,10 @@ return function(world: ty.World) local networked_pairs = {} for component in world:each(ct.Networked) do - local name = world:get(component, jecs.Name) :: string + local name = assert(world:get(component, jecs.Name), "Invalid component") if components[name] == nil then - continue + error("Invalid component:"..name) + end storages[component] = {} @@ -32,29 +37,30 @@ return function(world: ty.World) for relation in world:each(ct.NetworkedPair) do local name = world:get(relation, jecs.Name) :: string if not components[name] then - continue + error("Invalid component") end table.insert(networked_pairs, relation) end for _, component in networked_components do - local name = world:get(component, jecs.Name) :: string + local name = world:get(component, jecs.Name) if not components[name] then + -- error("Invalid component") error(`Networked Component (%id{component}%name{name})`) end local is_tag = jecs.is_tag(world, component) local storage = storages[component] if is_tag then - world:added(component, function(entity) - storage[entity] = true - end) + world:added(component, function(entity) + storage[entity] = true + end) else - world:added(component, function(entity, _, value) - storage[entity] = value - end) - world:changed(component, function(entity, _, value) - storage[entity] = value - end) + world:added(component, function(entity, _, value) + storage[entity] = value + end) + world:changed(component, function(entity, _, value) + storage[entity] = value + end) end world:removed(component, function(entity) @@ -64,51 +70,55 @@ return function(world: ty.World) for _, relation in networked_pairs do world:added(relation, function(entity, id, value) - local is_tag = jecs.is_tag(world, id) - local storage = storages[id] - if not storage then - storage = {} - storages[id] = storage - end - if is_tag then - storage[entity] = true - else - storage[entity] = value - end - end) + local is_tag = jecs.is_tag(world, id) + local storage = storages[id] + if not storage then + storage = {} + storages[id] = storage + end + if is_tag then + storage[entity] = true + else + storage[entity] = value + end + end) - world:changed(relation, function(entity, id, value) - local is_tag = jecs.is_tag(world, id) - if is_tag then - return - end + world:changed(relation, function(entity, id, value) + local is_tag = jecs.is_tag(world, id) + if is_tag then + return + end - local storage = storages[id] - if not storage then - storage = {} - storages[id] = storage - end + local storage = storages[id] + if not storage then + storage = {} + storages[id] = storage + end - storage[entity] = value - end) + storage[entity] = value + end) - world:removed(relation, function(entity, id) - local storage = storages[id] - if not storage then - storage = {} - storages[id] = storage - end + world:removed(relation, function(entity, id) + local storage = storages[id] + if not storage then + storage = {} + storages[id] = storage + end - storage[entity] = "jecs.Remove" - end) + storage[entity] = "jecs.Remove" + end) end + -- local requested_snapshots = collect(remotes.request_snapshot.OnServerEvent) local players_added = collect(Players.PlayerAdded) return function() local snapshot_lazy: ty.Snapshot local set_ids_lazy: { jecs.Entity } + -- In the future maybe it should be requested by the player instead when they + -- are ready to receive the replication. Otherwise streaming could be complicated + -- with intances references being nil. for player in players_added do if not snapshot_lazy then snapshot_lazy, set_ids_lazy = {}, {} @@ -126,7 +136,7 @@ return function(world: ty.World) if is_tag then set_values = table.create(entities_len, true) else - local column = archetype.columns[archetype.records[component]] + local column = archetype.columns_map[component] table.move(column, 1, entities_len, set_n + 1, set_values) end @@ -135,10 +145,18 @@ return function(world: ty.World) local set = table.move(set_ids_lazy, 1, set_n, 1, {}) - snapshot_lazy[tostring(component)] = { - set = if set_n > 0 then set else nil, - values = if set_n > 0 then set_values else nil, - } + local ser_id: string = nil :: any + + if jecs.IS_PAIR(component) then + ser_id = `{jecs.pair_first(world, component)},{jecs.pair_first(world, component)}` + else + ser_id = tostring(component) + end + + snapshot_lazy[ser_id] = { + set = if set_n > 0 then set else nil, + values = if set_n > 0 then set_values else nil, + } end end @@ -159,7 +177,7 @@ return function(world: ty.World) set_n += 1 set_ids[set_n] = e set_values[set_n] = v or true - elseif world:contains(e) then + elseif not world:contains(e) then removed_n += 1 removed_ids[removed_n] = e end @@ -176,7 +194,15 @@ return function(world: ty.World) if dirty then local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity } local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity } - snapshot[tostring(component)] = { + local ser_id: string = nil :: any + + if jecs.IS_PAIR(component) then + ser_id = `{jecs.pair_first(world, component)},{jecs.pair_second(world, component)}` + else + ser_id = tostring(component) + end + + snapshot[ser_id] = { set = if set_n > 0 then set else nil, values = if set_n > 0 then set_values else nil, removed = if removed_n > 0 then removed else nil diff --git a/test/tests.luau b/test/tests.luau index 6387d03..01a22fc 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -24,11 +24,6 @@ type Id = jecs.Id local entity_visualiser = require("@tools/entity_visualiser") local dwi = entity_visualiser.stringify -TEST("repro", function() - - -end) - TEST("bulk", function() local world = jecs.world() local A = world:component()