[Release] CommandLibrary - Command-Based Framework for Complex Game Logic

What is CommandLibrary

CommandLibrary is designed to organize complex game behaviors into modular, reusable components. It allows you to wrap your code into “Command” objects that can be scheduled, grouped, and prioritized. It is heavily inspired by the WPILib architecture used in FRC robotics.

CommandLibrary allows for modularity and easy debugging.

Commands are capable of being called from anywhere, and the scheduler automatically takes care of subsystem ownership, priority, and interruptions.

This library is designed to be used with a subsystem architecture.

Each Subsystem represents a separate system or resources, such as a weapon system. By including which subsystems a command requires, the scheduler can prevent two different pieces of logic from trying to control the same thing at the same time.

Commands themselves are cleaner than your ordinary code.

The library handles the lifecycle (Initialize, Execute, End), so you don’t have to manually manage “IsRunning” booleans or cleanup logic. When a command is interrupted or finishes, the End function runs automatically to clean up your state.

Why Did I Make This?

I made this because I had this hugggeee game idea and I was actually terrified by the sheer complexity and size of it. I got to thinking and had a revelation: to take what WPILib did, and PUT IT IN ROBLOX! Will it work? I have no idea. However, I think this library will be helpful to at least one person out there… Hopefully.

Note:

I would avoid using CommandLibrary until 1.1.0 comes out. There are a few things I need to smooth over, and I also might add decorators to allow for much simpler syntax.

CommandLibrary - Wally
CommandLibrary - Creator Store

7 Likes

Hallo! This is very very interesting. I wonder if there’re any examples or documentation, I’d love to see one.

1 Like

I included some API references in the readme, as well as a very small piece of example code. I’ll probably get around to creating some more detailed documentation soon.

2 Likes

Actually, I wouldn’t trust the example I wrote in the readme lol

Edit:
Here’s a better example
example.txt (2.4 KB)

2 Likes

I don’t know if this is a correct implementation. Let’s say I want to make a KeycardReader class

Subsystem
local Subsystem = require(CommandLibrary.Subsystem)

local ReaderSubsystem = {}
ReaderSubsystem.__index = ReaderSubsystem

function ReaderSubsystem.new(model, parent)
    local self = setmetatable({}, ReaderSubsystem)

    self.Subsystem = Subsystem.new("Reader:" .. model.Name)

    self.Model = model
    self.Primary = model.PrimaryPart
    self.Parent = parent

    self.Status = model:FindFirstChild("Status")
    self.Sound = self.Primary:FindFirstChildOfClass("Sound")

    return self
end

return ReaderSubsystem
Commands
--// ACCESS GRANTED
local Command = require(CommandLibrary.Command)

local GrantAccess = {}
GrantAccess.__index = GrantAccess
setmetatable(GrantAccess, Command)

function GrantAccess.new(reader)
    local self = setmetatable(Command.new(), GrantAccess)

    self.reader = reader
    self:AddRequirements(reader.Subsystem)

    return self
end

function GrantAccess:Initialize()
    local r = self.reader

    r.Sound.SoundId = "rbxassetid://" .. r.GrantedSound
    r.Status.Material = Enum.Material.Neon
    r.Status.Color = Color3.fromRGB(0, 255, 150)
    r.Sound:Play()
end

return GrantAccess

Flow
local Sequential = CommandLibrary.SequentialCommandGroup
local WaitCommand = CommandLibrary.WaitCommand

local function AccessGrantedFlow(reader, duration)
    return Sequential.new(
        GrantAccess.new(reader),
        WaitCommand.new(duration),
    )
end
Class
local Players = game:GetService("Players")
local CommandScheduler = require(CommandLibrary.CommandScheduler)

local Reader = {}
Reader.__index = Reader

local function getPlayer(tool: Tool): Player?
    local char = tool.Parent
    if not char then return end
    return Players:GetPlayerFromCharacter(char)
end

function Reader.new(model: Model, parent, config)
    local self = setmetatable({}, Reader)

    self.Subsystem = ReaderSubsystem.new(model, parent)
    self.Primary = model.PrimaryPart
    self.Parent = parent

    self.Duration = config.Duration or 5

    self:_bindTouch()

    return self
end

function Reader:_bindTouch()
    self.Primary.Touched:Connect(function(hit)
        local tool = hit.Parent
        if not tool:IsA("Tool") then return end

        local player = getPlayer(tool)
        if not player then return end

        local perms = self.Parent:ReadPermissions()
        local ok = Authenticate(player, perms)

        if ok then
            CommandScheduler.schedule(
                AccessGrantedFlow(self.Subsystem, self.Duration)
            )
        end
    end)
end

return Reader

Subsystem is okay as-is

Commands
local Command = require(CommandLibrary.Command)
local GrantAccess = {}
GrantAccess.__index = GrantAccess
setmetatable(GrantAccess, Command)

function GrantAccess.new(readerSubsystem, grantedSound)
    local self = setmetatable(Command.new(), GrantAccess)
    self.readerSubsystem = readerSubsystem
    self.grantedSound = grantedSound or "rbxassetid://"
    self:AddRequirements(readerSubsystem.Subsystem)
    return self
end

function GrantAccess:Initialize()
    local r = self.readerSubsystem
    if r.Sound then
        r.Sound.SoundId = self.grantedSound
        r.Sound:Play()
    end
    if r.Status then
        r.Status.Material = Enum.Material.Neon
        r.Status.Color = Color3.fromRGB(0, 255, 150)
    end
end

return GrantAccess
Flow
local Sequential = CommandLibrary.SequentialCommandGroup
local WaitCommand = CommandLibrary.WaitCommand
local InstantCommand = CommandLibrary.InstantCommand

local function AccessGrantedFlow(readerSubsystem, duration, grantedSound)
    return Sequential.new(
        GrantAccess.new(readerSubsystem, grantedSound),
        WaitCommand.new(duration),
        InstantCommand.new(function()
            if readerSubsystem.Status then
                readerSubsystem.Status.Material = Enum.Material.Plastic
                readerSubsystem.Status.Color = Color3.fromRGB(255, 255, 255)
            end
        end)
    )
end
Class
function Reader.new(model: Model, parent, config)
    local self = setmetatable({}, Reader)
    self.Subsystem = ReaderSubsystem.new(model, parent)
    self.Primary = model.PrimaryPart
    self.Parent = parent
    self.Duration = config.Duration or 5
    self.GrantedSound = config.GrantedSound or "rbxassetid://"
    
    self:_bindTouch()
    return self
end

function Reader:_bindTouch()
    self.Primary.Touched:Connect(function(hit)
        local tool = hit.Parent
        if not tool:IsA("Tool") then return end
        
        local player = getPlayer(tool)
        if not player then return end
        
        local perms = self.Parent:ReadPermissions()
        local ok = Authenticate(player, perms)
        
        if ok then
            CommandScheduler.schedule(
                AccessGrantedFlow(self.Subsystem, self.Duration, self.GrantedSound)
            )
        end
    end)
end

Although, I would recommend requiring the CommandLibrary module as whole so that you don’t need to require each part you need.

1 Like

I’ll try to make a more in-depth demonstration on how to use this library. The best I can say for now is to visit the WPILib docs in order to see how they use commands.

1 Like

Anyone seeing this:
Are there any features you’d like to see? I was considering creating a networker designed specifically for this. Version 1.1.0 will bring decorators, strict typing, bug fixes, and few other minor features regarding scheduling and subsystem ownership.

1 Like

Currently I just want to see a more in-depth demonstration or documentation as I’m very new into this style of programming and yet to understand this deep enough to provide you any concrete feedbacks. Maybe just do any features you seem fit :heart:

It is very interesting and I really like it. I’m also going to use this for my up coming project.

Perhaps you could make a Github repository for this, maybe one day I could contribute.

1 Like

Update on V1.1.0 (not released yet)

Decorators have been made, but they are around 10x less performant than a raw command. This is caused by each decorator creating a wrapper command. Instead, I will try injecting logic directly into the existing command.

Here are some current overhead statistics with raw commands:

  • Running 2000 agents simultaneously costs around 51.5 µs of CPU time per frame
  • CommandLib handles task allocation 2.08x faster than Roblox’s task.spawn. Task.spawn requires 1.54 µs per call to initialize a thread, while CommandLib schedules tasks in 0.74 µs
  • Each command instance uses around 240 bytes

Decorators:

  • A base command executes in 381 ns, and adding a 2-layer decorator chain increases that to 4.08 µs
  • Although slower, if you prioritize code readability over performance, this is nice to have
  • Decorators will hopefully be optimized when I release 1.1.0