Using the Fennel Programming Language to Make Games!

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!

5 Likes

Personally, I won’t use this because I’m already in love with Luau.
BUT! I can see how much effort you have put into this so, congrats!
Keep up the good work!

3 Likes

I don’t blame you haha, and also thank you!