Reviewing RenderModule before fully opening

I’d just like to know if there are any blatant errors or use case oversights with this module;

Render Module is a QoL “service simplifier” for easily updating large amounts of objects.

  • Can handle all basic RunService binds.
  • Primarily targetted at metamethod use case; Can also be used for module updating.
  • Can be called via the Server or the Client
  • Warns you if you’re trying to update the same object twice
  • Will default to updating via Heartbeat if no Priority has been assigned.

Methods

  • Render:Add(Object - table; Priority - string)
    Adds the Object to the pool found in Priority. Goes through some basic checks to ensure it will function correctly.
  • Render:BindAll()
    Binds all the Priorities found within RenderPool + creates a Heartbeat connection for Heartbeat.
  • Render:CeaseAll()
    Iterates through every table in the RenderPool, calling Object:Cease() if it has it, nullifying the pointer and removing it from the table.

.

RenderModule code
local RunService    = game:GetService("RunService")
local Server 		= RunService:IsServer()

local RENDER 		= {}

local RenderPool = not Server and {
    First 			= {};
    Input 			= {};
    Character 		= {};
    Camera 			= {};
    Last 			= {};
    Heartbeat 		= {};

	} or {
	Heartbeat 		= {};
	}

local HeartbeatCon 	= nil

function RENDER:BindAll()
	if HeartbeatCon ~= nil then warn("Attempting to rebind, returning") return end
    for Name, tbl in pairs (RenderPool) do
        if Name ~= "Heartbeat" then
            RunService:BindToRenderStep(Name, Enum.RenderPriority[Name].Value, function(dt)
            	for entry, object in pairs(tbl) do
                	object:Update(dt)
                    if object[1] == "Cease" then
                    	object:Cease()
                    	table.remove(tbl, entry)
                	end
                end
            end)
        else
            HeartbeatCon = RunService.Heartbeat:Connect(function(dt)
                for entry, object in pairs(tbl) do
                    object:Update(dt)
                    if object[1] == "Cease" then
                        object:Cease()
                        table.remove(tbl, entry)
                    end
                end
            end)
        end
    end
end

function Obliterate(tbl)
    for entry, object in pairs(tbl) do
        if object and object["Cease"] and type(object["Cease"]) == "function" then
            object:Cease()
        end
        object = nil
        table.remove(tbl, entry)
    end
end

function RENDER:CeaseAll()
    --Unbind and disconnect all
    for Name, Con in pairs (RenderPool) do
		RunService:UnbindFromRenderStep(Name)
		Obliterate(Con)
    end
	HeartbeatCon:Disconnect()
	HeartbeatCon = nil
end

function RENDER:Add(Object, Priority)
    local Pool

    --No object case
    local ENV = getfenv(2)["script"]:GetFullName()
    assert(Object, string.format("'%s' tried to add something to the Render without an object!",   ENV))
    assert(Object["Update"] and type(Object["Update"]) == "function",  string.format("'%s' passed an object without an Update function!",  ENV))
	
	--Object already exists case
	for _, tbl in pairs(RenderPool) do
		for entry, obj in pairs(tbl) do
			if obj == Object then
				warn("We're being passed an object already getting updates")
				warn(string.format("Returning to the script: %s",   ENV))
				return
			end
		end
	end
	--Check/Set Priority
    if not Priority then
        warn("Object added to Render without priority, defaulting to Heartbeat")
        Priority =  "Heartbeat"
    elseif not RenderPool[Priority] then
		warn(not Server and string.format("'%s' isn't a valid priority", Priority) or "We are the server, only Heartbeat available.")
		Priority =  "Heartbeat"
    end
    Pool = RenderPool[Priority]

    --Finally add to the pool
    table.insert(Pool, Object)
end

RENDER:BindAll()

return RENDER
ClientTest code
local Render = require(game.ReplicatedStorage:WaitForChild("RenderModule"))

local Item = {}

local Item_mt = {__index = Item}

function Item.New()

local self = {}

self.Speech = "Hullo there"

self.LifeTime = 0

return setmetatable(self, Item_mt)

end

function Item:Update(dt)

--Action

print(self.Speech)

self.LifeTime = self.LifeTime + dt

--Should we cease?

if self.LifeTime > 5 then

self[1] = "Cease"

end

end

function Item:Cease()

--Action

print("Oh no! I have to go!")

self = nil

end

local Ayy = Item.New()

wait(2)

Render:Add(Ayy) --Adds an Item without a priority reference. Add us to Heartbeat

wait(2)

Render:Add(Ayy, "Camera") --Tries to add an Item already in a RenderPool, we'll bounce back.
ServerTest code
local Render = require(game.ReplicatedStorage:WaitForChild("RenderModule"))

local Item = {}

local Item_mt = {__index = Item}

function Item.New()

local self = {}

self.Speech = "Hullo there"

self.LifeTime = 0

return setmetatable(self, Item_mt)

end

function Item:Update(dt)

--Action

print(self.Speech)

self.LifeTime = self.LifeTime + dt

--Should we cease?

if self.LifeTime > 5 then

self[1] = "Cease"

end

end

function Item:Cease()

--Action

print("Oh no! I have to go!")

self = nil

end

local Ayy = Item.New()

wait(2)

Render:Add(Ayy, "Camera") --Tests if we can add things to different priorities.

wait(2)

Render:BindAll() --Trys to bind all the priority calls, we are already bound, so we fail and return.

wait()

Render:CeaseAll() --Trys to unbind all our binds, succeeds.

Render:BindAll() --Trys to bind all the priority calls, we are no longer bound, so we succeed.

I’m curious to know;

  • Would this help you?
  • Are there any problems with this module you can see that I’ve missed?
  • Are there any additional functions you’d want out of this kind of simplifier?
  • What are your use cases?

I’ve been using a system similair to this for a while now, figured I should make it publicly available if it helps!

3 Likes

My first impression is that I am not too sure which development problems this module solves.

I am not personally keen on having to alter the structure of my objects - or indeed use “objects” in the first place - in order to use a system that is aimed at simplifying an existing API. I think that generally a simplifying API shouldn’t require the user to modify how they structure their own data/logic in such a fundamental way. However, I understand this clearly meets some of your own use cases. I would be interested to hear about them!

I also have a few points about the code itself. First off, where’s RunService.Stepped? This is a very useful signal, for example when overwriting or layering over Roblox animations. Remember that both the server and the client can access this.

Having the developer pass a string ‘priority’ will most likely lead to some elusive bugs. Because you are mapping the string directly to the associated RenderPriority EnumItem, multiple binds will exist at the same priority (for example, camera and character scripts already use these priorities directly). The order in which the binds at the same priority are called is actually undefined - the engine will choose “randomly” (source).

The reason that the actual integer values of these RenderPriority EnumItems have large gaps between them (e.g. First = 0, Input = 100) is so that developers can assign unique priorities to multiple bindings and guarantee their order. Your module should let developers choose their priority precisely so they can reason correctly about execution order when it matters.

I understand that this whole reply may come off as very negative - I can assure you this is not my intention or my overall impression of your work! I think creating open source resources is an excellent thing to do :slight_smile:

1 Like

The problem its aimed at solving is pretty niche in truth. I have a lot of things to update, rather than create a RunService bind/connection for each one, I can just add it to a renderpool and it will automatically start with the next frame.

Brutal use case: Time effect / Pausing

I’ve taken this as an excerpt from my code in Brutal, the only additions to the code presented here is establishing a GameSpeed multiplier for the deltas


local GameSpeed = 1 -- This should actually point to a value held by another module.
local GamePaused = false
RenderPool.Priority = {["Game"] = {} ; ["Independant"] = {}; }

--Per render update;
local gdt = dt * GameSpeed -- This is placed before starting iterations through the table
if GamePause == false then
--Iterate through this priorities "Game" table, pass :Update(gdt) instead of :Update(dt)
end
--Iterate through this priorities' "Independant" variable

This allows me to alter game speed on the fly and if I ever need to pause the game, I just don’t update the functions within the game table.

Yeah, missed that completely. Rarely use it, but would be useful to have.

Something I thought about, I’m not sure how I’d implement this fully. Add another function to create an additional priority bind from a string, from a number?

I’d be able to add functions to the updates; Rather than calling
Object:Update(dt)
it would just call
Object(dt)
but I’m not sure if I’d have a method of closing them properly.
If this was added, would you find it any more useful?

Without critique and feedback, we’d get nowhere! Thank you :slight_smile: