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,11 +1,12 @@
local types = require("../types") --!strict
local jecs = require(game:GetService("ReplicatedStorage").ecs) local remotes = require("./remotes")
local remotes = require("../remotes") local jecs = require("@jecs")
local collect = require("../collect") local collect = require("@modules/collect")
local components = require("../components")
-- this is just an example, you need an actually populated map
local components: { [string]: jecs.Id } = {}
local client_ids: {[jecs.Entity]: jecs.Entity } = {} local client_ids: {[jecs.Entity]: jecs.Entity<nil> } = {}
local function ecs_ensure_entity(world: jecs.World, id: jecs.Entity) local function ecs_ensure_entity(world: jecs.World, id: jecs.Entity)
local e = 0 local e = 0
@ -13,7 +14,7 @@ local function ecs_ensure_entity(world: jecs.World, id: jecs.Entity)
local ser_id = id local ser_id = id
local deser_id = client_ids[ser_id] local deser_id = client_ids[ser_id]
if deser_id then if deser_id then
if deser_id == 0 then if deser_id == 0::any then
local new_id = world:entity() local new_id = world:entity()
client_ids[ser_id] = new_id client_ids[ser_id] = new_id
deser_id = new_id deser_id = new_id
@ -51,7 +52,7 @@ end
-- return jecs.pair(rel, tgt) -- return jecs.pair(rel, tgt)
-- end -- end
local function ecs_deser_pairs(world, rel, tgt) local function ecs_deser_pairs(world, rel: jecs.Entity, tgt: jecs.Entity)
rel = ecs_ensure_entity(world, rel) rel = ecs_ensure_entity(world, rel)
tgt = ecs_ensure_entity(world, tgt) tgt = ecs_ensure_entity(world, tgt)
@ -67,7 +68,7 @@ return function(world: jecs.World)
for snapshot in snapshots do for snapshot in snapshots do
for ser_id, map in snapshot do for ser_id, map in snapshot do
local id = (tonumber(ser_id) :: any) :: jecs.Entity local id = (tonumber(ser_id) :: any) :: jecs.Entity
if jecs.IS_PAIR(id) and map.pair then if jecs.IS_PAIR(id) and map.pair == true then
id = ecs_deser_pairs(world, map.relation, map.target) id = ecs_deser_pairs(world, map.relation, map.target)
elseif id then elseif id then
id = ecs_ensure_entity(world, id) id = ecs_ensure_entity(world, id)

View file

@ -1,15 +1,17 @@
--!strict --!strict
local Players = game:GetService("Players") local Players = require("@game/Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage") local remotes = require("./remotes")
local ct = require(ReplicatedStorage.components) local jecs = require("@jecs")
local components = ct :: { [string]: jecs.Entity } local collect = require("@modules/collect")
local remotes = require(ReplicatedStorage.remotes) local ty = require("./types")
local jecs = require(ReplicatedStorage.ecs)
local collect = require(ReplicatedStorage.collect) -- this is just an example, you need an actually populated map
local ty = require(ReplicatedStorage.types) local components: { [string]: jecs.Id } = {}
local ct = components
return function(world: jecs.World) return function(world: jecs.World)
local storages = {} :: { [jecs.Entity]: {[jecs.Entity]: any }} local storages = {} :: { [jecs.Id]: {[jecs.Id]: any }}
local networked_components = {} local networked_components = {}
local networked_pairs = {} local networked_pairs = {}
@ -106,14 +108,14 @@ return function(world: jecs.World)
return function(_, dt: number) return function(_, dt: number)
local snapshot_lazy: ty.snapshot local snapshot_lazy: ty.snapshot
local set_ids_lazy: { jecs.Entity } local set_ids_lazy: { jecs.Id }
-- In the future maybe it should be requested by the player instead when they -- 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 -- are ready to receive the replication. Otherwise streaming could be complicated
-- with intances references being nil. -- 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 = {}::any, {}
for component, storage in storages do for component, storage in storages do
local set_values = {} local set_values = {}
@ -133,7 +135,7 @@ return function(world: jecs.World)
set_n += entities_len set_n += entities_len
end end
local set = table.move(set_ids_lazy, 1, set_n, 1, {}) local set = table.move(set_ids_lazy, 1, set_n, 1, {}::any)
local map = { local map = {
set = if set_n > 0 then set else nil, set = if set_n > 0 then set else nil,
@ -214,7 +216,7 @@ return function(world: jecs.World)
snapshot[tostring(component)] = map snapshot[tostring(component)] = map
end end
end end
if next(snapshot) ~= nil then if next(snapshot::any) ~= nil then
remotes.replication:FireAllClients(snapshot) remotes.replication:FireAllClients(snapshot)
-- print(snapshot) -- print(snapshot)
end end

View file

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

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,5 +1,3 @@
--!strict
local function collect(signal) local function collect(signal)
local enqueued = {} local enqueued = {}

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>
}