Replace demo with advanced examples

This commit is contained in:
Ukendio 2025-11-30 08:59:04 +01:00
parent 553cb89b10
commit 8e06781be6
21 changed files with 438 additions and 771 deletions

6
demo/.gitignore vendored
View file

@ -1,6 +0,0 @@
# Project place file
/example.rbxlx
# Roblox Studio lock files
/*.rbxlx.lock
/*.rbxl.lock

View file

@ -1,15 +0,0 @@
# Demo
## Build with Rojo
To build the place, run the following commands from the root of the repository:
```bash
cd demo
rojo build -o "demo.rbxl"
```
Next, open `demo.rbxl` in Roblox Studio and start the Rojo server:
```bash
rojo serve
```

View file

@ -1,55 +0,0 @@
{
"name": "demo",
"emitLegacyScripts": false,
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"$path": "src/ReplicatedStorage",
"ecs": {
"$path": "../jecs.luau"
},
"Packages": {
"$path": "Packages"
}
},
"ServerScriptService": {
"$className": "ServerScriptService",
"$path": "src/ServerScriptService"
},
"Workspace": {
"$properties": {
"FilteringEnabled": true
},
"Baseplate": {
"$className": "Part",
"$properties": {
"Anchored": true,
"Color": [0.38823, 0.37254, 0.38823],
"Locked": true,
"Position": [0, -10, 0],
"Size": [512, 20, 512]
}
}
},
"Lighting": {
"$properties": {
"Ambient": [0, 0, 0],
"Brightness": 2,
"GlobalShadows": true,
"Outlines": false,
"Technology": "Voxel"
}
},
"SoundService": {
"$properties": {
"RespectFilteringEnabled": true
}
},
"StarterPlayer": {
"StarterPlayerScripts": {
"$path": "src/StarterPlayer/StarterPlayerScripts"
}
}
}
}

View file

@ -1,65 +0,0 @@
--!strict
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local types = require("./types")
local Networked = jecs.tag()
local NetworkedPair = jecs.tag()
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 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 = 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 :: {[string]: types.Id<any> } do
jecs.meta(component, jecs.Name, name)
end
return components

View file

@ -1,10 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local schedule = require(ReplicatedStorage.schedule)
local heartbeat = schedule(world,
systems.entities_delete,
systems.replication
)
game:GetService("RunService").Heartbeat:Connect(heartbeat)

View file

@ -1,91 +0,0 @@
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jabby = require(ReplicatedStorage.Packages.jabby)
local ct = require(ReplicatedStorage.components)
local jecs = require(ReplicatedStorage.ecs)
jabby.set_check_function(function(player) return true end)
local scheduler = jabby.scheduler.create()
jabby.register({
applet = jabby.applets.scheduler,
name = "Scheduler",
configuration = {
scheduler = scheduler,
},
}::any)
local ContextActionService = game:GetService("ContextActionService")
local function create_widget(_, state: Enum.UserInputState): Enum.ContextActionResult
local client = jabby.obtain_client()
if state ~= Enum.UserInputState.Begin then
return Enum.ContextActionResult.Pass
end
client.spawn_app(client.apps.home::any, nil)
return Enum.ContextActionResult.Sink
end
local RunService = game:GetService("RunService")
local function schedule(world, ...)
local function get_entity_from_part(part: BasePart): (jecs.Entity<any>?, PVInstance?)
for id, model in world:query(ct.Renderable) do
if not part:IsDescendantOf(model) then continue end
return id, model
end
return nil, nil
end
jabby.register({
applet = jabby.applets.world,
name = "World",
configuration = {
world = world,
get_entity_from_part = get_entity_from_part,
},
}::any)
local systems = { ... }
local function systems_load(mod: ModuleScript, ...)
local fn = require(mod) :: (...any) -> ()
local system = fn(...) or fn
local system_id = scheduler:register_system({
name = mod.Name,
module = mod,
})
return {
system = system,
id = system_id
}
end
for i, mod in systems do
systems[i] = systems_load(mod, world, 0)
end
if RunService:IsClient() then
ContextActionService:BindAction(
"Open Jabby Home",
create_widget,
false,
Enum.KeyCode.F4
)
end
return function(dt: number, input: InputObject?)
for i, config in systems do
-- config.system(world, dt, input)
local system = config.system
local id = config.id
scheduler:run(id, system,
world, dt, input)
end
end
end
return schedule

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

@ -1,14 +0,0 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
export type World = typeof(jecs.world())
export type Entity = jecs.Entity
export type Id<T> = jecs.Id<T>
export type Snapshot = {
[string]: {
set: { jecs.Entity }?,
values: { any }?,
removed: { jecs.Entity }?
}
}
return {}

View file

@ -1,21 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local jecs = require(ReplicatedStorage.ecs)
local schedule = require(ReplicatedStorage.schedule)
require(ReplicatedStorage.components)
local world = jecs.world()
local systems = ServerScriptService.systems
local heartbeat = schedule(world,
systems.players_added,
systems.poison_hurts,
systems.health_regen,
systems.lifetimes_expire,
systems.life_is_painful,
systems.entities_delete,
systems.replication
)
game:GetService("RunService").Heartbeat:Connect(heartbeat)

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

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

@ -1,24 +0,0 @@
local collect = require("../../ReplicatedStorage/collect")
local types = require("../../ReplicatedStorage/types")
local ct = require("../../ReplicatedStorage/components")
local Players = game:GetService("Players")
local player_added = collect(Players.PlayerAdded)
return function(world: types.World, dt: number)
for player in player_added do
local entity = world:entity()
world:set(entity, ct.Player, player)
end
for entity, player in world:query(ct.Player):without(ct.Renderable) do
local character = player.Character
if not character then
continue
end
if not character.Parent then
continue
end
world:set(entity, ct.Renderable, character)
end
end

View file

@ -1,18 +0,0 @@
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, 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
if math.random() < 1 / 60 / 1 and health > 1 then
world:set(tgt, ct.Health, health - 1)
end
end
end

View file

@ -1,8 +0,0 @@
[package]
name = "marcus/demo"
version = "0.1.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
[dependencies]
jabby = "alicesaidhi/jabby@0.2.2"

View file

@ -1,121 +1,122 @@
local types = require("../types")
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local remotes = require("../remotes")
local collect = require("../collect")
local components = require("../components")
local client_ids: {[jecs.Entity]: jecs.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
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
e = deser_id
return e
end
-- 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_str(world, token)
-- local tokens = string.split(token, ",")
-- local rel = tonumber(tokens[1]) :: jecs.Entity
-- local tgt = tonumber(tokens[2]) :: jecs.Entity
-- rel = ecs_ensure_entity(world, rel)
-- tgt = ecs_ensure_entity(world, tgt)
-- return jecs.pair(rel, tgt)
-- end
local function ecs_deser_pairs(world, rel, tgt)
rel = ecs_ensure_entity(world, rel)
tgt = ecs_ensure_entity(world, tgt)
return jecs.pair(rel, tgt)
end
local snapshots = collect(remotes.replication.OnClientEvent)
return function(world: jecs.World)
for entity in world:each(components.Destroy) do
client_ids[entity] = nil
end
for snapshot in snapshots do
for ser_id, map in snapshot do
local id = (tonumber(ser_id) :: any) :: jecs.Entity
if jecs.IS_PAIR(id) and map.pair then
id = ecs_deser_pairs(world, map.relation, map.target)
elseif id then
id = ecs_ensure_entity(world, id)
end
-- if not id then
-- id = ecs_deser_pairs_str(world, ser_id)
-- else
-- id = ecs_ensure_entity(world, id)
-- end
local members = world:get(id, components.NetworkedMembers)
local set = map.set
if set then
if jecs.is_tag(world, id) then
for _, entity in set do
entity = ecs_ensure_entity(world, entity)
world:add(entity, id)
end
else
local values = map.values :: { any }
for i, entity in set do
entity = ecs_ensure_entity(world, entity)
local value = values[i]
if members then
for _, member in members do
local data = value[member] :: {jecs.Entity} | jecs.Entity -- targets
if typeof(data) == "table" then
for pos, tgt in data :: { jecs.Entity } do
data[pos] = ecs_ensure_entity(world, tgt)
end
else
value[member] = ecs_ensure_entity(world, data :: any)
end
end
end
world:set(entity, id, value)
end
end
end
local removed = map.removed
if removed then
for _, entity in removed do
entity = ecs_ensure_entity(world, entity)
world:remove(entity, id)
end
end
end
end
end
--!strict
local remotes = require("./remotes")
local jecs = require("@jecs")
local collect = require("@modules/collect")
-- this is just an example, you need an actually populated map
local components: { [string]: jecs.Id } = {}
local client_ids: {[jecs.Entity]: jecs.Entity<nil> } = {}
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::any 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
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
e = deser_id
return e
end
-- 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_str(world, token)
-- local tokens = string.split(token, ",")
-- local rel = tonumber(tokens[1]) :: jecs.Entity
-- local tgt = tonumber(tokens[2]) :: jecs.Entity
-- rel = ecs_ensure_entity(world, rel)
-- tgt = ecs_ensure_entity(world, tgt)
-- return jecs.pair(rel, tgt)
-- end
local function ecs_deser_pairs(world, rel: jecs.Entity, tgt: jecs.Entity)
rel = ecs_ensure_entity(world, rel)
tgt = ecs_ensure_entity(world, tgt)
return jecs.pair(rel, tgt)
end
local snapshots = collect(remotes.replication.OnClientEvent)
return function(world: jecs.World)
for entity in world:each(components.Destroy) do
client_ids[entity] = nil
end
for snapshot in snapshots do
for ser_id, map in snapshot do
local id = (tonumber(ser_id) :: any) :: jecs.Entity
if jecs.IS_PAIR(id) and map.pair == true then
id = ecs_deser_pairs(world, map.relation, map.target)
elseif id then
id = ecs_ensure_entity(world, id)
end
-- if not id then
-- id = ecs_deser_pairs_str(world, ser_id)
-- else
-- id = ecs_ensure_entity(world, id)
-- end
local members = world:get(id, components.NetworkedMembers)
local set = map.set
if set then
if jecs.is_tag(world, id) then
for _, entity in set do
entity = ecs_ensure_entity(world, entity)
world:add(entity, id)
end
else
local values = map.values :: { any }
for i, entity in set do
entity = ecs_ensure_entity(world, entity)
local value = values[i]
if members then
for _, member in members do
local data = value[member] :: {jecs.Entity} | jecs.Entity -- targets
if typeof(data) == "table" then
for pos, tgt in data :: { jecs.Entity } do
data[pos] = ecs_ensure_entity(world, tgt)
end
else
value[member] = ecs_ensure_entity(world, data :: any)
end
end
end
world:set(entity, id, value)
end
end
end
local removed = map.removed
if removed then
for _, entity in removed do
entity = ecs_ensure_entity(world, entity)
world:remove(entity, id)
end
end
end
end
end

View file

@ -1,222 +1,224 @@
--!strict
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components)
local components = ct :: { [string]: jecs.Entity }
local remotes = require(ReplicatedStorage.remotes)
local jecs = require(ReplicatedStorage.ecs)
local collect = require(ReplicatedStorage.collect)
local ty = require(ReplicatedStorage.types)
return function(world: jecs.World)
local storages = {} :: { [jecs.Entity]: {[jecs.Entity]: any }}
local networked_components = {}
local networked_pairs = {}
for component in world:each(ct.Networked) do
local name = world:get(component, jecs.Name)
assert(name)
if components[name] == nil then
error("Invalid component:"..name)
end
storages[component] = {}
table.insert(networked_components, component)
end
for relation in world:each(ct.NetworkedPair) do
local name = world:get(relation, jecs.Name)
assert(name)
if not components[name] then
error("Invalid component")
end
table.insert(networked_pairs, relation)
end
for _, component in networked_components do
local name = world:get(component, jecs.Name)
if not name or 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)
else
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)
storage[entity] = "jecs.Remove"
end)
end
for _, relation in networked_pairs do
world:added(relation, function(entity: jecs.Entity, id: jecs.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)
world:changed(relation, function(entity: jecs.Id, id: jecs.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
storage[entity] = value
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)
end
-- local requested_snapshots = collect(remotes.request_snapshot.OnServerEvent)
local players_added = collect(Players.PlayerAdded)
return function(_, dt: number)
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 = {}, {}
for component, storage in storages do
local set_values = {}
local set_n = 0
local q = world:query(component)
local is_tag = jecs.is_tag(world, component)
for _, archetype in q:archetypes() do
local entities = archetype.entities
local entities_len = #entities
table.move(entities, 1, entities_len, set_n + 1, set_ids_lazy)
if not is_tag then
local column = archetype.columns_map[component]
table.move(column, 1, entities_len, set_n + 1, set_values)
end
set_n += entities_len
end
local set = table.move(set_ids_lazy, 1, set_n, 1, {})
local map = {
set = if set_n > 0 then set else nil,
values = if set_n > 0 then set_values else nil,
}
if jecs.IS_PAIR(component) then
map.relation = jecs.pair_first(world, component)
map.target = jecs.pair_second(world, component)
map.pair = true
end
snapshot_lazy[tostring(component)] = map
end
end
remotes.replication:FireClient(player, snapshot_lazy)
end
-- accumulator += dt
-- Purposely sending less diffs of the world because doing it at 60hz
-- gets expensive. But this requires interpolated elements in the scene.
-- if accumulator > 1/60 then
-- accumulator = 0
local snapshot = {} :: ty.snapshot
local set_ids = {}
local removed_ids = {}
for component, storage in storages do
local set_values = {} :: { any }
local set_n = 0
local removed_n = 0
for e, v in storage do
if v ~= "jecs.Remove" then
set_n += 1
set_ids[set_n] = e
set_values[set_n] = v or true
elseif world:contains(e) then
removed_n += 1
removed_ids[removed_n] = e
end
end
table.clear(storage)
local dirty = false
if set_n > 0 or removed_n > 0 then
dirty = true
end
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 }
-- 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
local map = {
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,
}
if jecs.IS_PAIR(component) then
map.relation = jecs.pair_first(world, component)
map.target = jecs.pair_second(world, component)
map.pair = true
end
snapshot[tostring(component)] = map
end
end
if next(snapshot) ~= nil then
remotes.replication:FireAllClients(snapshot)
-- print(snapshot)
end
end
end
--!strict
local Players = require("@game/Players")
local remotes = require("./remotes")
local jecs = require("@jecs")
local collect = require("@modules/collect")
local ty = require("./types")
-- this is just an example, you need an actually populated map
local components: { [string]: jecs.Id } = {}
local ct = components
return function(world: jecs.World)
local storages = {} :: { [jecs.Id]: {[jecs.Id]: any }}
local networked_components = {}
local networked_pairs = {}
for component in world:each(ct.Networked) do
local name = world:get(component, jecs.Name)
assert(name)
if components[name] == nil then
error("Invalid component:"..name)
end
storages[component] = {}
table.insert(networked_components, component)
end
for relation in world:each(ct.NetworkedPair) do
local name = world:get(relation, jecs.Name)
assert(name)
if not components[name] then
error("Invalid component")
end
table.insert(networked_pairs, relation)
end
for _, component in networked_components do
local name = world:get(component, jecs.Name)
if not name or 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)
else
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)
storage[entity] = "jecs.Remove"
end)
end
for _, relation in networked_pairs do
world:added(relation, function(entity: jecs.Entity, id: jecs.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)
world:changed(relation, function(entity: jecs.Id, id: jecs.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
storage[entity] = value
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)
end
-- local requested_snapshots = collect(remotes.request_snapshot.OnServerEvent)
local players_added = collect(Players.PlayerAdded)
return function(_, dt: number)
local snapshot_lazy: ty.snapshot
local set_ids_lazy: { jecs.Id }
-- 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 = {}::any, {}
for component, storage in storages do
local set_values = {}
local set_n = 0
local q = world:query(component)
local is_tag = jecs.is_tag(world, component)
for _, archetype in q:archetypes() do
local entities = archetype.entities
local entities_len = #entities
table.move(entities, 1, entities_len, set_n + 1, set_ids_lazy)
if not is_tag then
local column = archetype.columns_map[component]
table.move(column, 1, entities_len, set_n + 1, set_values)
end
set_n += entities_len
end
local set = table.move(set_ids_lazy, 1, set_n, 1, {}::any)
local map = {
set = if set_n > 0 then set else nil,
values = if set_n > 0 then set_values else nil,
}
if jecs.IS_PAIR(component) then
map.relation = jecs.pair_first(world, component)
map.target = jecs.pair_second(world, component)
map.pair = true
end
snapshot_lazy[tostring(component)] = map
end
end
remotes.replication:FireClient(player, snapshot_lazy)
end
-- accumulator += dt
-- Purposely sending less diffs of the world because doing it at 60hz
-- gets expensive. But this requires interpolated elements in the scene.
-- if accumulator > 1/60 then
-- accumulator = 0
local snapshot = {} :: ty.snapshot
local set_ids = {}
local removed_ids = {}
for component, storage in storages do
local set_values = {} :: { any }
local set_n = 0
local removed_n = 0
for e, v in storage do
if v ~= "jecs.Remove" then
set_n += 1
set_ids[set_n] = e
set_values[set_n] = v or true
elseif world:contains(e) then
removed_n += 1
removed_ids[removed_n] = e
end
end
table.clear(storage)
local dirty = false
if set_n > 0 or removed_n > 0 then
dirty = true
end
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 }
-- 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
local map = {
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,
}
if jecs.IS_PAIR(component) then
map.relation = jecs.pair_first(world, component)
map.target = jecs.pair_second(world, component)
map.pair = true
end
snapshot[tostring(component)] = map
end
end
if next(snapshot::any) ~= nil then
remotes.replication:FireAllClients(snapshot)
-- print(snapshot)
end
end
end

View file

@ -1,5 +1,5 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../ReplicatedStorage/types")
local ReplicatedStorage = require("@game/ReplicatedStorage")
local types = require("./types")
type Remote<T...> = {
FireClient: (Remote<T...>, Player, T...) -> (),
@ -31,12 +31,6 @@ end
return {
input = datagram_ensure("input") :: Remote<string>,
replication = stream_ensure("replication") :: Remote<{
[string]: {
set: { types.Entity }?,
values: { any }?,
removed: { types.Entity }?
}
}>,
replication = stream_ensure("replication") :: Remote<types.snapshot>,
}

19
examples/networking/types.luau Executable file
View file

@ -0,0 +1,19 @@
local jecs = require("@jecs")
export type snapshot = {
[string]: {
set: { jecs.Entity }?,
values: { any }?,
removed: { jecs.Entity }?,
pair: true,
relation: jecs.Entity,
target: jecs.Entity
} | {
set: { jecs.Entity }?,
values: { any }?,
removed: { jecs.Entity }?,
pair: false,
}
}
return {}

View file

@ -1,33 +1,31 @@
--!strict
local function collect(signal)
local enqueued = {}
local i = 0
local connection = signal:Connect(function(...)
table.insert(enqueued, { ... })
i += 1
end)
return function(): any
if i == 0 then
return
end
i -= 1
local args: any = table.remove(enqueued, 1)
return unpack(args)
end, connection
end
type Signal<T... = ...any> = {
Connect: (self: Signal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
ConnectParallel: (self: Signal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
Once: (self: Signal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
Wait: (self: Signal<T...>) -> (T...)
}
return collect :: <T...>(Signal<T...>) -> (() -> (T...), RBXScriptConnection)
local function collect(signal)
local enqueued = {}
local i = 0
local connection = signal:Connect(function(...)
table.insert(enqueued, { ... })
i += 1
end)
return function(): any
if i == 0 then
return
end
i -= 1
local args: any = table.remove(enqueued, 1)
return unpack(args)
end, connection
end
type Signal<T... = ...any> = {
Connect: (self: Signal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
ConnectParallel: (self: Signal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
Once: (self: Signal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
Wait: (self: Signal<T...>) -> (T...)
}
return collect :: <T...>(Signal<T...>) -> (() -> (T...), RBXScriptConnection)

39
modules/remotes.luau Executable file
View file

@ -0,0 +1,39 @@
-- A simple way to safely type remote events without hassle
local ReplicatedStorage = require("@game/ReplicatedStorage")
local jecs = require("@jecs")
local ty = require("./")
type Remote<T...> = {
FireClient: (Remote<T...>, Player, T...) -> (),
FireAllClients: (Remote<T...>, T...) -> (),
FireServer: (Remote<T...>, T...) -> (),
OnServerEvent: RBXScriptSignal<(Player, T...)>,
OnClientEvent: RBXScriptSignal<T...>
}
local function stream_ensure(name)
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("RemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote
end
local function datagram_ensure(name)
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("UnreliableRemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote
end
return {
input = datagram_ensure("input") :: Remote<string>,
replication = stream_ensure("replication") :: Remote<snapshot>
}