Introduction to Behavorial Programming

Necessary Knowledge

  • Basic Lua
  • Coroutines
  • Event driven programming

What is Behavorial Programming?

Behavorial Programming (short: BP) is a niche paradigm, so the resources on it are very scarce, which is why im writing this tutorial.

At its core, BP is a kind of event driven programming. But instead of “dispatching” events, events are “requested” and those requests can be blocked.
Also, logic is contained in so called “b threads”, which is just a nice word for “coroutine”. I’ll explain that later in this topic in detail.
Each thread requests, wait for and blocks some events.
For example, a thread could request the event “Withdraw Money”. Another thread may wait for the event “Withdraw Money” and block it if more money is withdrawn than is in the acount.

A BP Program uses a “Scheduler”, which schedules the BThreads (aka decides when and which events are dispatched and bthreads are run).
Each BThread has a priority. BThreads with higher priority are run first.

How is logic added/modified?

This opens up a new way to modify/add logic:

  1. Read about the events that trigger the logic in documentation
  2. Add a Bthread
  3. Block events if you want to restrict logic
  4. Request new events if you want to trigger new logic
  5. Wait for events if you want to “listen” to an event
  6. Add the BThread to the Scheduler with some priority

Whats the point?

A major pro of this is that you don’t need to dig into code to modify logic. For example, if you wanted to prevent the player from “running” if they’re “wounded” then you would:

  1. Add new BThread
  2. Wait for “run” event (the “run” event sets the “running” state of the player to true)
  3. Check if the player is Wounded, if Wounded == true then block the “run” event
  4. Add the BThread to the Scheduler with a priority higher than the BThread that increases the player speed

Don’t worry if this is hard to understand, its a weird way to code. Especially when normally code in OOP or procedurally.

Visualization

This Picture visualizes what the Program does when selecting viable events:

  1. BThreads request, block and wait for events.
  2. From the requested events, all that are not blocked are viable to get selected.
  3. The non blocked, requested event that is requested by the highest priority thread is selected.
  4. All Threads that wait for or requested (!) the event are informed.
    → this basically triggers a coroutine.resume(bthread)
  5. Go to 1.

Something which may not be self-evident is that even BThreads that only requested an event without waiting for the event get notified too. This is quite useful in practice: After the event got dispatched, the BThread can continue do something afterwards.

Code

For example, an account could be coded. For this i import the module and create a scheduler with BP.new(). The money is external to the Scheduler.
BP is only a way to write logic, not data. That means data can be structured in many ways: store it in variables (procedural), in objects (OOP) or in reactive structures (reactive) etc.

local BPModule = require(game:GetService("ReplicatedStorage").BP)
local BP = BPModule.BP

local money = 0

local Account = BP.new()

The user can deposit money. For this, the BThread waits for a “deposit” event. Events can have payloads, aka data. The deposit event has a payload of type number which represents the money the user wants to deposit. We simply add the payload to the money.
The current event data can be accessed with Account.payload. I know thats not as clean as having a callback function that takes the data, but this way makes it more flexible, which i’ll elaborate later.

We give the BThread a priority of 0.

BP:addBThread(function()
	while true do
		coroutine.yield {
			waitFor = {"Deposit"}
		}
		money += Account.payload
		print("user now has " .. money .. " money")
	end, 0)

Next, we want the user to be able to withdraw money. For this, we wait for a “Withdraw” event and then subtract the payload from the users money.

We give the BThread a priority of 0 also.

BP:addBThread(function()
	while true do
		coroutine.yield {
			waitFor = {"Withdraw"}
		}
		money -= Account.payload
		print("user now has " .. money .. " money")
	end
end, 0)

We can now withdraw and deposit. We can either do it externally or with another BThread.

--This BThread requests Deposit events. A request table is of the form {[string]: any}
--where the key is the event name and any the payload
BP:addBThread(function()
	coroutine.yield {
		request = {
			Deposit = 50
		}
	}
	assert(money == 50)
	coroutine.yield {
		request = {
			 Withdraw = 40
		}
	}
	assert(money == 10)
	coroutine.yield {
		request = {
			Withdraw = 20
		}
	}
	assert(money == -10)
end, 0)

--alternatively, request it extenerally
Account:requestEvent("Deposit", 20)
Account:requestEvent("Withdraw", 10)

To run the program, we call Account:run(). Run takes 2 optional parameters:

  1. bool: should the program exit when all requested events are blocked?
  2. function: executes each run where all requested events are blocked

For the purpose of this, we don’t need the second argument so we’ll leave it empty. The first one defaults to true, which is fine so we don’t need it too.

Account:run()

assuming we first run the Account and then request the events externally, this gets printed:

user now has 50 money
user now has 10 money
user now has -10 money
user now has 10 money
user now has 0 money

Where Behavorial Programming Shines

Imagine your boss wants you to implement a new requirement:

  • The User should not be able to withdraw more than they have
  • The User should get notified if they try to overdraw the account

Imagine also, that you never worked on this project. You would first have to read and understand the code in most paradigms and then add a boolean check/new state etc.

In BP you only need a high level view of what you want to do:

  • Block the event "Withdraw" when Account.payload > money
  • Request the event "Overdraw" when Account.payload > money
  • Display error message when the event "Overdraw" gets dispatched

To implement this requirement, a new BThread is added.

The BThread needs to be run before the BThread that withdraws the money from the players account (money variable). The BThread that handles the withdrawing logic has a priority 0, so we give the new BThread a priority of 1.

The BThread needs to first wait for a "Withdraw" event and then block "Withdraw" until the event "Overdraw" is dispatched and then repeat this.

BP:addBThread(function()
	while true do
		coroutine.yield {
			waitFor = {"Withdraw"}
		}
		if Account.payload > money then
			coroutine.yield {
				block = {"Withdraw"},
				request = {Overdraw = Account.payload - money}
			}
		end
	end
end, 1)

The 2. yield is interesting since we have both a block and request. Generalizing this to a yield that waits for some set of events W, requests some set of events R and blocks some set of events B, in plain english such yield would mean the following:

Block all events in set B until exactly one event in set R or exactly one event in set W is dispatched

In the upper code, we block "Withdraw" until a "Overdraw" event is dispatched.

This example captures the most important feature of BP:

The previous code written by the “others” wasn’t touched.
Only a high level overview of priority of BThreads, what the BThreads do and which events do what and carry which data is necessary.
Any logic can be added or modified solely by creating new BThreads.
Modifying previously created BThreads is not necessary.

Lua Libraries for BP

This paradigm is like really really niche.
Theres no BP libraries for Lua that i found.

I did code a functional(?) BP scheduler, which is have in a github repo.

I don’t recommend using it. I didn’t test it roughly.

Its more like an example on how to implement a scheduler.

If you try to implement your own BP library, be aware that a table can’t be modified while iterating over it.
That caused me trouble in my first implementations and required me to loop over all threads for blocks, requests etc. which may seem ineffiecent, but that’s necessary.

5 Likes