Compare commits

..

1 commit

Author SHA1 Message Date
Laptev Stanislav
dcd2ac8508
Merge ca10a8a8f3 into 53f705ac2e 2025-07-01 15:24:42 +03:00
13 changed files with 127 additions and 250 deletions

View file

@ -2,7 +2,7 @@
--!native --!native
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter) local Matter = require(ReplicatedStorage.DevPackages.matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr) local ecr = require(ReplicatedStorage.DevPackages.ecr)
local newWorld = Matter.World.new() local newWorld = Matter.World.new()

View file

@ -1,5 +1,3 @@
--!strict
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
local types = require("./types") local types = require("./types")
@ -7,58 +5,31 @@ local types = require("./types")
local Networked = jecs.tag() local Networked = jecs.tag()
local NetworkedPair = jecs.tag() local NetworkedPair = jecs.tag()
local InstanceMapping = jecs.component() :: jecs.Id<Instance> local Renderable = jecs.component() :: jecs.Id<Instance>
jecs.meta(InstanceMapping, jecs.OnAdd, function(component) jecs.meta(Renderable, Networked)
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 Renderable = jecs.component() :: types.Id<Instance> local Poison = jecs.component() :: jecs.Id<number>
local Poison = jecs.component() :: types.Id<number> jecs.meta(Poison, Networked)
local Health = jecs.component() :: types.Id<number>
local Player = jecs.component() :: types.Id<Player> local Health = jecs.component() :: jecs.Id<number>
local Debuff = jecs.tag() :: types.Entity jecs.meta(Health, Networked)
local Lifetime = jecs.component() :: types.Id<{
duration: number, local Player = jecs.component() :: jecs.Id<Player>
created: number jecs.meta(Player, Networked)
}>
local Destroy = jecs.tag()
local components = { local components = {
Renderable = networked_id(instance_mapping_id(Renderable)), Renderable = Renderable,
Player = networked_id(Player), Player = Player,
Poison = networked_id(Poison), Poison = Poison,
Health = networked_id(Health), Health = 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, Networked = Networked,
NetworkedPair = NetworkedPair, NetworkedPair = NetworkedPair,
} }
for name, component in components :: {[string]: types.Id<any> } do for name, component in components do
jecs.meta(component, jecs.Name, name) jecs.meta(component, jecs.Name, name)
end end

View file

@ -10,5 +10,4 @@ local world = observers_add(jecs.world())
local systems = ReplicatedStorage.systems local systems = ReplicatedStorage.systems
SYSTEM(world, systems.receive_replication) SYSTEM(world, systems.receive_replication)
SYSTEM(world, systems.entities_delete)
RUN(world) RUN(world)

View file

@ -1,13 +0,0 @@
--!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

View file

@ -2,51 +2,45 @@ local types = require("../types")
local jecs = require(game:GetService("ReplicatedStorage").ecs) local jecs = require(game:GetService("ReplicatedStorage").ecs)
local remotes = require("../remotes") local remotes = require("../remotes")
local collect = require("../collect") local collect = require("../collect")
local components = require("../components") local client_ids = {}
local client_ids: {[jecs.Entity]: jecs.Entity } = {} local function ecs_map_get(world, id)
local deserialised_id = client_ids[id]
local function ecs_ensure_entity(world: jecs.World, id: jecs.Entity) if not deserialised_id then
local e = 0 if world:has(id, jecs.Name) then
deserialised_id = world:entity(id)
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
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 else
if world:contains(ser_id) and world:get(ser_id, jecs.Name) then deserialised_id = world:entity()
deser_id = ser_id
else
deser_id = world:entity()
end
end end
client_ids[ser_id] = deser_id
client_ids[id] = deserialised_id
end end
e = deser_id -- 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
return e return deserialised_id
end end
-- local rel_render = `e{jecs.ECS_ID(rel)}v{jecs.ECS_GENERATION(rel)}` local function ecs_make_alive_id(world, id)
-- local tgt_render = `e{jecs.ECS_ID(tgt)}v{jecs.ECS_GENERATION(tgt)}` local rel = jecs.ECS_PAIR_FIRST(id)
local function ecs_deser_pairs(world, token) local tgt = jecs.ECS_PAIR_SECOND(id)
local tokens = string.split(token, ",")
local rel = tonumber(tokens[1])
local tgt = tonumber(tokens[2])
rel = ecs_ensure_entity(world, rel) rel = ecs_map_get(world, rel)
tgt = ecs_ensure_entity(world, tgt) tgt = ecs_map_get(world, tgt)
return jecs.pair(rel, tgt) return jecs.pair(rel, tgt)
end end
@ -54,31 +48,25 @@ end
local snapshots = collect(remotes.replication.OnClientEvent) local snapshots = collect(remotes.replication.OnClientEvent)
return function(world: types.World) return function(world: types.World)
for entity in world:each(components.Destroy) do
client_ids[entity] = nil
end
for snapshot in snapshots do for snapshot in snapshots do
for ser_id, map in snapshot do for id, map in snapshot do
local id = tonumber(ser_id) id = tonumber(id)
if not id then if jecs.IS_PAIR(id) then
id = ecs_deser_pairs(world, ser_id) id = ecs_make_alive_id(world, id)
else
id = ecs_ensure_entity(world, id)
end end
local set = map.set local set = map.set
if set then if set then
if jecs.is_tag(world, id) then if jecs.is_tag(world, id) then
for _, entity in set do for _, entity in set do
entity = ecs_ensure_entity(world, entity) entity = ecs_map_get(world, entity)
world:add(entity, id) world:add(entity, id)
end end
else else
local t = os.clock()
local values = map.values local values = map.values
for i, entity in set do for i, entity in set do
entity = ecs_ensure_entity(world, entity) entity = ecs_map_get(world, entity)
world:set(entity, id, values[i]) world:set(entity, id, values[i])
end end
end end
end end
@ -87,7 +75,7 @@ return function(world: types.World)
if removed then if removed then
for _, entity in removed do for _, entity in removed do
entity = ecs_ensure_entity(world, entity) entity = ecs_map_get(world, entity)
if not world:contains(entity) then if not world:contains(entity) then
continue continue
end end

View file

@ -15,8 +15,5 @@ local systems = ServerScriptService.systems
SYSTEM(world, systems.replication) SYSTEM(world, systems.replication)
SYSTEM(world, systems.players_added) SYSTEM(world, systems.players_added)
SYSTEM(world, systems.poison_hurts) SYSTEM(world, systems.poison_hurts)
SYSTEM(world, systems.health_regen)
SYSTEM(world, systems.lifetimes_expire)
SYSTEM(world, systems.life_is_painful) SYSTEM(world, systems.life_is_painful)
SYSTEM(world, ReplicatedStorage.systems.entities_delete)
RUN(world, 0) RUN(world, 0)

View file

@ -1,15 +0,0 @@
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

View file

@ -1,19 +1,12 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components) local ct = require(ReplicatedStorage.components)
local types = require(ReplicatedStorage.types) local types = require(ReplicatedStorage.types)
local jecs = require(ReplicatedStorage.ecs)
return function(world: types.World, dt: number) return function(world: types.World, dt: number)
if math.random() < (1 / 60 / 7) then for e in world:query(ct.Player):without(ct.Health) do
for e in world:each(ct.Health) do world:set(e, ct.Health, 100)
local poison = world:entity() end
world:add(poison, ct.Debuff) for e in world:query(ct.Player, ct.Health):without(ct.Poison) do
world:add(poison, jecs.pair(jecs.ChildOf, e)) world:set(e, ct.Poison, 10)
world:set(poison, ct.Poison, 10)
world:set(poison, ct.Lifetime, {
duration = 3,
created = os.clock()
})
end
end end
end end

View file

@ -1,12 +0,0 @@
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

View file

@ -12,13 +12,9 @@ return function(world: types.World, dt: number)
for entity, player in world:query(ct.Player):without(ct.Renderable) do for entity, player in world:query(ct.Player):without(ct.Renderable) do
local character = player.Character local character = player.Character
if not character then if character then
continue
end
if not character.Parent then if not character.Parent then
continue world:set(entity, ct.Renderable, character)
end end
world:set(entity, ct.Renderable, character)
end end
end end

View file

@ -1,18 +1,12 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components) local ct = require(ReplicatedStorage.components)
local types = require(ReplicatedStorage.types) return function(world, dt)
local jecs = require(ReplicatedStorage.ecs) for e, poison, health in world:query(ct.Poison, ct.Health) do
local health_after_tick = health - poison * dt * 0.05
return function(world: types.World, dt: number) if health_after_tick < 0 then
for e, poison_tick in world:query(ct.Poison, jecs.pair(jecs.ChildOf, jecs.w)) do world:remove(e, ct.Health)
local tgt = world:target(e, jecs.ChildOf)
local health = world:get(tgt, ct.Health)
if not health then
continue continue
end 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
end end

View file

@ -1,13 +1,9 @@
--!strict
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components) local types = require("../../ReplicatedStorage/types")
local components = ct :: { [string]: jecs.Entity } local ct = require("../../ReplicatedStorage/components")
local remotes = require(ReplicatedStorage.remotes)
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
local collect = require(ReplicatedStorage.collect) local remotes = require("../../ReplicatedStorage/remotes")
local ty = require(ReplicatedStorage.types) local components = ct :: {[string]: jecs.Entity }
return function(world: ty.World) return function(world: ty.World)
@ -23,10 +19,9 @@ return function(world: ty.World)
local networked_pairs = {} local networked_pairs = {}
for component in world:each(ct.Networked) do for component in world:each(ct.Networked) do
local name = assert(world:get(component, jecs.Name), "Invalid component") local name = world:get(component, jecs.Name) :: string
if components[name] == nil then if components[name] == nil then
error("Invalid component:"..name) continue
end end
storages[component] = {} storages[component] = {}
@ -37,30 +32,29 @@ return function(world: ty.World)
for relation in world:each(ct.NetworkedPair) do for relation in world:each(ct.NetworkedPair) do
local name = world:get(relation, jecs.Name) :: string local name = world:get(relation, jecs.Name) :: string
if not components[name] then if not components[name] then
error("Invalid component") continue
end end
table.insert(networked_pairs, relation) table.insert(networked_pairs, relation)
end end
for _, component in networked_components do for _, component in networked_components do
local name = world:get(component, jecs.Name) local name = world:get(component, jecs.Name) :: string
if not components[name] then if not components[name] then
-- error("Invalid component")
error(`Networked Component (%id{component}%name{name})`) error(`Networked Component (%id{component}%name{name})`)
end end
local is_tag = jecs.is_tag(world, component) local is_tag = jecs.is_tag(world, component)
local storage = storages[component] local storage = storages[component]
if is_tag then if is_tag then
world:added(component, function(entity) world:added(component, function(entity)
storage[entity] = true storage[entity] = true
end) end)
else else
world:added(component, function(entity, _, value) world:added(component, function(entity, _, value)
storage[entity] = value storage[entity] = value
end) end)
world:changed(component, function(entity, _, value) world:changed(component, function(entity, _, value)
storage[entity] = value storage[entity] = value
end) end)
end end
world:removed(component, function(entity) world:removed(component, function(entity)
@ -70,55 +64,51 @@ return function(world: ty.World)
for _, relation in networked_pairs do for _, relation in networked_pairs do
world:added(relation, function(entity, id, value) world:added(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id) local is_tag = jecs.is_tag(world, id)
local storage = storages[id] local storage = storages[id]
if not storage then if not storage then
storage = {} storage = {}
storages[id] = storage storages[id] = storage
end end
if is_tag then if is_tag then
storage[entity] = true storage[entity] = true
else else
storage[entity] = value storage[entity] = value
end end
end) end)
world:changed(relation, function(entity, id, value) world:changed(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id) local is_tag = jecs.is_tag(world, id)
if is_tag then if is_tag then
return return
end end
local storage = storages[id] local storage = storages[id]
if not storage then if not storage then
storage = {} storage = {}
storages[id] = storage storages[id] = storage
end end
storage[entity] = value storage[entity] = value
end) end)
world:removed(relation, function(entity, id) world:removed(relation, function(entity, id)
local storage = storages[id] local storage = storages[id]
if not storage then if not storage then
storage = {} storage = {}
storages[id] = storage storages[id] = storage
end end
storage[entity] = "jecs.Remove" storage[entity] = "jecs.Remove"
end) end)
end end
-- local requested_snapshots = collect(remotes.request_snapshot.OnServerEvent)
local players_added = collect(Players.PlayerAdded) local players_added = collect(Players.PlayerAdded)
return function() return function()
local snapshot_lazy: ty.Snapshot local snapshot_lazy: ty.Snapshot
local set_ids_lazy: { jecs.Entity } 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 for player in players_added do
if not snapshot_lazy then if not snapshot_lazy then
snapshot_lazy, set_ids_lazy = {}, {} snapshot_lazy, set_ids_lazy = {}, {}
@ -136,7 +126,7 @@ return function(world: ty.World)
if is_tag then if is_tag then
set_values = table.create(entities_len, true) set_values = table.create(entities_len, true)
else else
local column = archetype.columns_map[component] local column = archetype.columns[archetype.records[component]]
table.move(column, 1, entities_len, set_n + 1, set_values) table.move(column, 1, entities_len, set_n + 1, set_values)
end end
@ -145,18 +135,10 @@ return function(world: ty.World)
local set = table.move(set_ids_lazy, 1, set_n, 1, {}) local set = table.move(set_ids_lazy, 1, set_n, 1, {})
local ser_id: string = nil :: any snapshot_lazy[tostring(component)] = {
set = if set_n > 0 then set else nil,
if jecs.IS_PAIR(component) then values = if set_n > 0 then set_values else nil,
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
end end
@ -177,7 +159,7 @@ return function(world: ty.World)
set_n += 1 set_n += 1
set_ids[set_n] = e set_ids[set_n] = e
set_values[set_n] = v or true set_values[set_n] = v or true
elseif not world:contains(e) then elseif world:contains(e) then
removed_n += 1 removed_n += 1
removed_ids[removed_n] = e removed_ids[removed_n] = e
end end
@ -194,15 +176,7 @@ return function(world: ty.World)
if dirty then if dirty then
local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity } local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity }
local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity } local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity }
local ser_id: string = nil :: any snapshot[tostring(component)] = {
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, set = if set_n > 0 then set else nil,
values = if set_n > 0 then set_values else nil, values = if set_n > 0 then set_values else nil,
removed = if removed_n > 0 then removed else nil removed = if removed_n > 0 then removed else nil

View file

@ -24,6 +24,11 @@ type Id<T=unknown> = jecs.Id<T>
local entity_visualiser = require("@tools/entity_visualiser") local entity_visualiser = require("@tools/entity_visualiser")
local dwi = entity_visualiser.stringify local dwi = entity_visualiser.stringify
TEST("repro", function()
end)
TEST("bulk", function() TEST("bulk", function()
local world = jecs.world() local world = jecs.world()
local A = world:component() local A = world:component()