mirror of
https://github.com/Ukendio/jecs.git
synced 2025-07-06 14:49:17 +00:00
Update networking example
Some checks are pending
Some checks are pending
This commit is contained in:
parent
53f705ac2e
commit
a6ba9f4bd5
13 changed files with 252 additions and 129 deletions
|
@ -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()
|
||||
|
||||
|
|
|
@ -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<Instance>
|
||||
jecs.meta(Renderable, Networked)
|
||||
local InstanceMapping = jecs.component() :: jecs.Id<Instance>
|
||||
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<number>
|
||||
jecs.meta(Poison, Networked)
|
||||
|
||||
local Health = jecs.component() :: jecs.Id<number>
|
||||
jecs.meta(Health, Networked)
|
||||
|
||||
local Player = jecs.component() :: jecs.Id<Player>
|
||||
jecs.meta(Player, Networked)
|
||||
|
||||
local Renderable = jecs.component() :: types.Id<Instance>
|
||||
local Poison = jecs.component() :: types.Id<number>
|
||||
local Health = jecs.component() :: types.Id<number>
|
||||
local Player = jecs.component() :: types.Id<Player>
|
||||
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<any> } do
|
||||
jecs.meta(component, jecs.Name, name)
|
||||
end
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
13
demo/src/ReplicatedStorage/systems/entities_delete.luau
Executable file
13
demo/src/ReplicatedStorage/systems/entities_delete.luau
Executable file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
15
demo/src/ServerScriptService/systems/health_regen.luau
Executable file
15
demo/src/ServerScriptService/systems/health_regen.luau
Executable file
|
@ -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
|
|
@ -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
|
||||
|
|
12
demo/src/ServerScriptService/systems/lifetimes_expire.luau
Executable file
12
demo/src/ServerScriptService/systems/lifetimes_expire.luau
Executable file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -24,11 +24,6 @@ type Id<T=unknown> = jecs.Id<T>
|
|||
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()
|
||||
|
|
Loading…
Reference in a new issue