mirror of
				https://github.com/Ukendio/jecs.git
				synced 2025-11-04 10:59:18 +00:00 
			
		
		
		
	Self contained changetracker
This commit is contained in:
		
							parent
							
								
									18ead3ed19
								
							
						
					
					
						commit
						d5de1ad425
					
				
					 2 changed files with 136 additions and 106 deletions
				
			
		| 
						 | 
				
			
			@ -935,7 +935,7 @@ do
 | 
			
		|||
    setmetatable(it, it)
 | 
			
		||||
 | 
			
		||||
    function world_query(world: World, ...: any): Query
 | 
			
		||||
    	-- breaking?
 | 
			
		||||
    	-- breaking
 | 
			
		||||
    	if (...) == nil then
 | 
			
		||||
    		error("Missing components")
 | 
			
		||||
    	end
 | 
			
		||||
| 
						 | 
				
			
			@ -1161,26 +1161,28 @@ World.parent = world_parent
 | 
			
		|||
 | 
			
		||||
function World.new()
 | 
			
		||||
    local self = setmetatable({
 | 
			
		||||
            archetypeIndex = {} :: { [string]: Archetype },
 | 
			
		||||
            archetypes = {} :: Archetypes,
 | 
			
		||||
    		componentIndex = {} :: ComponentIndex,
 | 
			
		||||
    		entityIndex = {
 | 
			
		||||
    			dense = {} :: { [i24]: i53 },
 | 
			
		||||
    			sparse = {} :: { [i53]: Record },
 | 
			
		||||
    		} :: EntityIndex,
 | 
			
		||||
    		hooks = {
 | 
			
		||||
    			[EcsOnAdd] = {},
 | 
			
		||||
    		},
 | 
			
		||||
    		nextArchetypeId = 0,
 | 
			
		||||
    		nextComponentId = 0,
 | 
			
		||||
    		nextEntityId = 0,
 | 
			
		||||
    		ROOT_ARCHETYPE = (nil :: any) :: Archetype,
 | 
			
		||||
		}, World)
 | 
			
		||||
        archetypeIndex = {} :: { [string]: Archetype },
 | 
			
		||||
        archetypes = {} :: Archetypes,
 | 
			
		||||
  		componentIndex = {} :: ComponentIndex,
 | 
			
		||||
  		entityIndex = {
 | 
			
		||||
 			dense = {} :: { [i24]: i53 },
 | 
			
		||||
 			sparse = {} :: { [i53]: Record },
 | 
			
		||||
  		} :: EntityIndex,
 | 
			
		||||
  		hooks = {
 | 
			
		||||
 			[EcsOnAdd] = {},
 | 
			
		||||
  		},
 | 
			
		||||
  		nextArchetypeId = 0,
 | 
			
		||||
  		nextComponentId = 0,
 | 
			
		||||
  		nextEntityId = 0,
 | 
			
		||||
  		ROOT_ARCHETYPE = (nil :: any) :: Archetype,
 | 
			
		||||
    }, World)
 | 
			
		||||
 | 
			
		||||
	self.ROOT_ARCHETYPE = archetype_of(self, {})
 | 
			
		||||
 | 
			
		||||
	-- Initialize built-in components
 | 
			
		||||
	entity_index_new_id(self.entityIndex, EcsChildOf)
 | 
			
		||||
	for i = HI_COMPONENT_ID + 1, EcsRest do
 | 
			
		||||
	   -- Initialize built-in components
 | 
			
		||||
		entity_index_new_id(self.entityIndex, i)
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	return self
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										204
									
								
								test/tests.luau
									
									
									
									
									
								
							
							
						
						
									
										204
									
								
								test/tests.luau
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -564,10 +564,48 @@ TEST("world", function()
 | 
			
		|||
 | 
			
		||||
end)
 | 
			
		||||
 | 
			
		||||
type Tracker<T> = { track: (world: World, fn: (changes: {
 | 
			
		||||
        added: () -> () -> (number, T),
 | 
			
		||||
        removed: () -> () -> number,
 | 
			
		||||
        changed: () -> () -> (number, T, T)
 | 
			
		||||
    }) -> ()) -> ()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST("changetracker", function()
 | 
			
		||||
    local world = jecs.World.new()
 | 
			
		||||
    local Previous = world:component()
 | 
			
		||||
type Entity<T = any> = number & { __nominal_type_dont_use: T }
 | 
			
		||||
 | 
			
		||||
local ChangeTracker: <T>(component: Entity<T>) -> Tracker<T>
 | 
			
		||||
 | 
			
		||||
do
 | 
			
		||||
    local world: World
 | 
			
		||||
    local T
 | 
			
		||||
    local PreviousT
 | 
			
		||||
    local addedComponents
 | 
			
		||||
    local removedComponents
 | 
			
		||||
    local isTrivial
 | 
			
		||||
    local added
 | 
			
		||||
    local removed
 | 
			
		||||
 | 
			
		||||
    local function changes_added()
 | 
			
		||||
        added = true
 | 
			
		||||
        local q = world:query(T):without(PreviousT)
 | 
			
		||||
        return function()
 | 
			
		||||
            local id, data = q:next()
 | 
			
		||||
            if not id then
 | 
			
		||||
                return nil
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            if isTrivial == nil then
 | 
			
		||||
                isTrivial = typeof(data) ~= "table"
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            if not isTrivial then
 | 
			
		||||
                data = table.clone(data)
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            addedComponents[id] = data
 | 
			
		||||
            return id, data
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local function shallowEq(a, b)
 | 
			
		||||
        for k, v in a do
 | 
			
		||||
| 
						 | 
				
			
			@ -578,112 +616,102 @@ TEST("changetracker", function()
 | 
			
		|||
        return true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local function ChangeTracker(world, component)
 | 
			
		||||
        local addedComponents = {}
 | 
			
		||||
        local removedComponents = {}
 | 
			
		||||
        local previous = jecs.pair(Previous, component)
 | 
			
		||||
        local isTrivial = nil
 | 
			
		||||
    local function changes_changed()
 | 
			
		||||
        local q = world:query(T, PreviousT)
 | 
			
		||||
 | 
			
		||||
        local function track(fn)
 | 
			
		||||
            local added = false
 | 
			
		||||
            local removed = false
 | 
			
		||||
 | 
			
		||||
            local changes = {}
 | 
			
		||||
            function changes.added()
 | 
			
		||||
                added = true
 | 
			
		||||
                local q = world:query(component):without(previous)
 | 
			
		||||
                return function()
 | 
			
		||||
                    local id, data = q:next()
 | 
			
		||||
                    if not id then
 | 
			
		||||
                        return nil
 | 
			
		||||
                    end
 | 
			
		||||
 | 
			
		||||
                    if isTrivial == nil then
 | 
			
		||||
                        isTrivial = typeof(data) ~= "table"
 | 
			
		||||
                    end
 | 
			
		||||
 | 
			
		||||
                    if not isTrivial then
 | 
			
		||||
                        data = table.clone(data)
 | 
			
		||||
                    end
 | 
			
		||||
 | 
			
		||||
                    addedComponents[id] = data
 | 
			
		||||
                    return id, data
 | 
			
		||||
        return function()
 | 
			
		||||
            local id, new, old = q:next()
 | 
			
		||||
            while true do
 | 
			
		||||
                if not id then
 | 
			
		||||
                    return nil
 | 
			
		||||
                end
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            function changes.changed()
 | 
			
		||||
                local q = world:query(component, previous)
 | 
			
		||||
 | 
			
		||||
                return function()
 | 
			
		||||
                    local id, new, old = q:next()
 | 
			
		||||
                    while true do
 | 
			
		||||
                        if not id then
 | 
			
		||||
                            return nil
 | 
			
		||||
                        end
 | 
			
		||||
 | 
			
		||||
                        if not isTrivial then
 | 
			
		||||
                            if not shallowEq(new, old) then
 | 
			
		||||
                                break
 | 
			
		||||
                            end
 | 
			
		||||
                        elseif new ~= old then
 | 
			
		||||
                            break
 | 
			
		||||
                        end
 | 
			
		||||
 | 
			
		||||
                        id, new, old = q:next()
 | 
			
		||||
                if not isTrivial then
 | 
			
		||||
                    if not shallowEq(new, old) then
 | 
			
		||||
                        break
 | 
			
		||||
                    end
 | 
			
		||||
 | 
			
		||||
                    addedComponents[id] = new
 | 
			
		||||
 | 
			
		||||
                    return id, old, new
 | 
			
		||||
                elseif new ~= old then
 | 
			
		||||
                    break
 | 
			
		||||
                end
 | 
			
		||||
 | 
			
		||||
                id, new, old = q:next()
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            function changes.removed()
 | 
			
		||||
                removed = true
 | 
			
		||||
            addedComponents[id] = new
 | 
			
		||||
 | 
			
		||||
                local q = world:query(previous):without(component)
 | 
			
		||||
                return function()
 | 
			
		||||
                    local id = q:next()
 | 
			
		||||
                    if id then
 | 
			
		||||
                    table.insert(removedComponents, id)
 | 
			
		||||
                    end
 | 
			
		||||
                    return id
 | 
			
		||||
                end
 | 
			
		||||
            return id, old, new
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local function changes_removed()
 | 
			
		||||
        removed = true
 | 
			
		||||
 | 
			
		||||
        local q = world:query(PreviousT):without(T)
 | 
			
		||||
        return function()
 | 
			
		||||
            local id = q:next()
 | 
			
		||||
            if id then
 | 
			
		||||
            table.insert(removedComponents, id)
 | 
			
		||||
            end
 | 
			
		||||
            return id
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
            fn(changes)
 | 
			
		||||
            if not added then
 | 
			
		||||
                for _ in changes.added() do
 | 
			
		||||
                end
 | 
			
		||||
            end
 | 
			
		||||
    local changes = {
 | 
			
		||||
        added = changes_added,
 | 
			
		||||
        changed = changes_changed,
 | 
			
		||||
        removed = changes_removed,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
            if not removed then
 | 
			
		||||
                for _ in changes.removed() do
 | 
			
		||||
                end
 | 
			
		||||
            end
 | 
			
		||||
    local function track(worldToTrack, fn)
 | 
			
		||||
        world = worldToTrack
 | 
			
		||||
        added = true
 | 
			
		||||
        removed = true
 | 
			
		||||
 | 
			
		||||
            for e, data in addedComponents do
 | 
			
		||||
                world:set(e, previous, if isTrivial then data else table.clone(data))
 | 
			
		||||
            end
 | 
			
		||||
        fn(changes)
 | 
			
		||||
 | 
			
		||||
            for _, e in removedComponents do
 | 
			
		||||
                world:remove(e, previous)
 | 
			
		||||
        if not added then
 | 
			
		||||
            for _ in changes_added() do
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            track = track
 | 
			
		||||
        }
 | 
			
		||||
        if not removed then
 | 
			
		||||
            for _ in changes_removed() do
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        for e, data in addedComponents do
 | 
			
		||||
            world:set(e, PreviousT, if isTrivial then data else table.clone(data))
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        for _, e in removedComponents do
 | 
			
		||||
            world:remove(e, PreviousT)
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local tracker = { track = track }
 | 
			
		||||
 | 
			
		||||
    function ChangeTracker<T>(component: Entity<T>): Tracker<T>
 | 
			
		||||
        T = component
 | 
			
		||||
        -- We just use jecs.Rest because people will probably not use it anyways
 | 
			
		||||
        PreviousT = jecs.pair(jecs.Rest, T)
 | 
			
		||||
        addedComponents = {}
 | 
			
		||||
        removedComponents = {}
 | 
			
		||||
 | 
			
		||||
        return tracker
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
TEST("changetracker", function()
 | 
			
		||||
    local world = jecs.World.new()
 | 
			
		||||
 | 
			
		||||
	do CASE "should allow change tracking"
 | 
			
		||||
        local Test = world:component()
 | 
			
		||||
        local TestTracker = ChangeTracker(world, Test)
 | 
			
		||||
        local Test = world:component() :: Entity<{ foo: number }>
 | 
			
		||||
        local TestTracker = ChangeTracker(Test)
 | 
			
		||||
 | 
			
		||||
		local e = world:entity()
 | 
			
		||||
		world:set(e, Test, { foo = 11 })
 | 
			
		||||
 | 
			
		||||
        TestTracker.track(function(changes)
 | 
			
		||||
        TestTracker.track(world, function(changes)
 | 
			
		||||
            local added = 0
 | 
			
		||||
            local changed = 0
 | 
			
		||||
            local removed = 0
 | 
			
		||||
| 
						 | 
				
			
			@ -705,7 +733,7 @@ TEST("changetracker", function()
 | 
			
		|||
    	    test.foo = test.foo + 1
 | 
			
		||||
     	end
 | 
			
		||||
 | 
			
		||||
        TestTracker.track(function(changes)
 | 
			
		||||
        TestTracker.track(world, function(changes)
 | 
			
		||||
            local added = 0
 | 
			
		||||
            local changed = 0
 | 
			
		||||
            local removed = 0
 | 
			
		||||
| 
						 | 
				
			
			@ -727,7 +755,7 @@ TEST("changetracker", function()
 | 
			
		|||
 | 
			
		||||
        world:remove(e, Test)
 | 
			
		||||
 | 
			
		||||
        TestTracker.track(function(changes)
 | 
			
		||||
        TestTracker.track(world, function(changes)
 | 
			
		||||
            local added = 0
 | 
			
		||||
            local changed = 0
 | 
			
		||||
            local removed = 0
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue