Howdy folks! It has been a while since I have posted anything on the forum… I regret not being as active as I would have liked to be. Either way, I am here today to present something I have been working on for around a week in my spare time. That being, I have gotten Fennel working inside of Roblox Studio! I have done this before, but this time its more intuitive and I am actually currently making a game using this setup.
What is Fennel?
Fennel is a Lisp style programming language that compiles to Lua. Its got a very consistent syntax, which I have come to absolutely adore. It boils down to everything is wrapped inside of ()
. Function calls always come at the start. For example; (fn say-hi [])
or if you wanted to add numbers (+ 2 3 4 5)
.
Why?
What a wonderful question! …I don’t know. A co-worker of mine makes fun of me for going through the effort of getting this working. I honestly don’t know why I did this. I am a die hard fan of Lua and I love the simplicity it brings, but I think Fennel really itches the spot in my brain for a need for structure. Lua leaves a lot of doors open and allows for there to be hundreds of ways to do anything. With Fennel it feels as if I am following instructions. Everything is done a specific way and I think this calms the anxious nerves in my brain.
Some Examples!
Enough of me venting my emotions on a public forum. Lets get into some examples!
An Object / Component
This code creates an object and binds various properties to that object.
Fennel
;;; Entity
(fn object []
"Creates an object with various components"
(let [self {}]
(set self.lifetime (tick))
(set self.collectionRange 5)
(set self.transform (CFrame.new))
(set self.texture "rbxassetid://")
(set self.model nil)
;; Return
self))
{: egg}
Here is the same code in Lua
Lua
local function object()
local self = {}
self.lifetime = tick()
self.collectionRange = 5
self.transform = CFrame.new()
self.texture = "rbxassetid://"
self.model = nil
return self
end
return {object = object}
You can see that in Fennel we use the let
keyword to define self. What this is doing is basically creating a variable in that specific scope. It makes it very hard for you to accidentally change a variable you shouldn’t have.
Something a Little More Simple
Heres some code I wrote up that gets the player and changes their walk speed.
Fennel
(local players (game:GetService "Players"))
(local player players.LocalPlayer)
(local character (or player.Character (player.CharacterAdded:Wait)))
(fn increase-speed [amount]
(set character.Humanoid.WalkSpeed (+ character.Humanoid.WalkSpeed amount)))
(increase-speed 25)
Lua
local players = game:GetService("Players")
local player = players.LocalPlayer
local character = (player.Character or (player.CharacterAdded):Wait())
local function increase_speed(amount)
character.Humanoid.WalkSpeed = (character.Humanoid.WalkSpeed + amount)
return nil
end
return increase_speed(25)
The Fennel code may look a little weird, but heres a general breakdown.
set
is the equivalent of =
. Functions and scripts always return something. Thats why you see it returning nil
or it returning the function at the end of the Lua code. local
variables are defined with either local
or let
. These variables cannot be changed though, if you want a variable to be able to be modified you can use the var
keyword.
The Compiler
Here is the code I wrote up in Lua to track any changes made to a Fennel file in the src directory and then compile it to Lua automatically. This assumes your doing a Rojo workflow. This mean you can just write code as you would normally and let Rojo and this Lua script do all the heavy lifting. I think another neat thing about this is it doesn’t require any dependencies. I’ve seen a lot of similar code but they end up using Lua File System. Another things to mention is this code only works on Windows, but I would love to get it working on multiple operating systems in the future.
--[[
TODO
* Files need to be removed from fileCache if they are deleted
]]
---- Variables
local fileCache = {}
---- Functions
-- Getters
local function getFennelFiles()
return io.popen([[where /r .\\src *.fnl]])
end
local function getFileContent(path)
local file = io.open(path, "rb")
if file then
local fileContent = string.gsub(file:read("*a"), "%s+", "")
file:close()
return fileContent
else
return fileCache[path]
end
end
-- Setters
local function setFileExtension(path)
return string.gsub(path, ".fnl", ".lua")
end
-- Booleans
local function isFileNew(path)
if fileCache[path] then
return false
else
return true
end
end
local function isFileChanged(path)
return fileCache[path] ~= getFileContent(path)
end
-- Actions
local function compileFile(path)
print("Compiling File...")
fileCache[path] = getFileContent(path)
os.execute([[fennel --compile ]] .. path .. [[ > ]] .. setFileExtension(path))
end
local function removeFile()
-- TODO
end
-- Main
local function watchFiles()
for path in getFennelFiles():lines() do
if isFileNew(path) or isFileChanged(path) then
compileFile(path)
end
end
--[[
TODO Check if file is up for removal from cache if its been deleted
]]
-- local startTime = os.clock()
-- while (startTime + 3) > os.clock() do end
os.execute([[ping -n 3 localhost > NUL]])
return watchFiles()
end
do
coroutine.wrap(watchFiles)()
end
Thank You!
Thank you guys and let me know if you have any question!