How can I learn to *engineer* code?

Recently, I have been trying to get back into luau coding in roblox. Over summer, I learned how to compact and modulize things, via oop, computational thinking, etc. However, I have come to a great realizing. I know all the syntax, but, I code very straightforward. I was trying to make a UIS wrapper, and my plan was to just simply do something like this:

function input.new(key:Enum.KeyCode, hold:boolean, hold_duration:number?)
	local self = setmetatable({}, input)
	
	self._key = key
	self._hold = hold
	self._hold_duration = hold_duration or 1
	
	return self
end

function input:check_device()
	return UIS:GetLastInputType()
end

function input:input_began(inp, gp)
	 if (inp.KeyCode == self._key) and (self._hold == false) then
		return true
	 elseif (inp.KeyCode == self._key) and (self._hold == true) then
		return false
	end
end

Now I understand this isn’t ACTUALLY what I would really do for this, but it just gets my idea across. However, after searching for references, and a little help from AI, I saw a UIS wrapper that I didn’t understand. As I said earlier, I understood all the syntax. But, it really just made me feel less of a programmer. Because I code so straight-forward, and I dont do well with systems. Here is said code:

local actions = {} 
local connections = {}  
local enabled = true

local function matchesBind(inputObject, bind)
	if bind.EnumType == Enum.KeyCode then
		return inputObject.KeyCode == bind
	elseif bind.EnumType == Enum.UserInputType then
		return inputObject.UserInputType == bind
	end
end

local function fireAction(action, state, inputObject)
	action.callback(state, inputObject)
end


local function onInputBegan(inputObject, gameProcessed)
	if gameProcessed or not enabled then return end

	for _, action in pairs(actions) do
		for _, bind in ipairs(action.keybinds) do
			if matchesBind(inputObject, bind) then
				action.held = true
				fireAction(action, Enum.UserInputState.Begin, inputObject)
				break
			end
		end
	end
end

local function onInputEnded(inputObject, gameProcessed)
	if not enabled then return end

	for _, action in pairs(actions) do
		if not action.held then continue end

		for _, bind in ipairs(action.keybinds) do
			if matchesBind(inputObject, bind) then
				action.held = false
				fireAction(action, Enum.UserInputState.End, inputObject)
				break
			end
		end
	end
end

function Input.Bind(actionName, keybinds, callback)
	assert(type(actionName) == "string", "Action name must be a string")
	assert(type(keybinds) == "table", "Keybinds must be a table")
	assert(type(callback) == "function", "Callback must be a function")

	if actions[actionName] then
		warn("Action already bound:", actionName)
		return
	end

	actions[actionName] = {
		keybinds = keybinds,
		callback = callback,
		held = false
	}
end

function Input.Unbind(actionName)
	actions[actionName] = nil
end

function Input.Enable()
	enabled = true
end

function Input.Disable()
	enabled = false

	-- Force-release all held inputs
	for _, action in pairs(actions) do
		if action.held then
			action.held = false
			action.callback(Enum.UserInputState.End, nil)
		end
	end
end

function Input.Clear()
	table.clear(actions)
end

connections.InputBegan =
	UserInputService.InputBegan:Connect(onInputBegan)

connections.InputEnded =
	UserInputService.InputEnded:Connect(onInputEnded)

function Input.GetLastInputType()
	return UserInputService:GetLastInputType()
end

return Input


return input

All I’m asking is for direction on how I can learn to code this systematically-and if possible-help me break this code down and understand it so I can use it in the future. Because its like I code without a mental model. I just apply the syntax that I know-no further thought. Any help is appreciated :cowboy_hat_face:

3 Likes

Don’t strive to pretend to write code.
Abstractions, sugar, bloat; these are bad crutches. Cargo-cult residue.

Read this.
https://devforum.roblox.com/t/%CE%B4-the-lost-art-of-optimization-what-do-we-gain-when-we-drop-oops-training-wheels/4123198

You are not inept for not being familiar with the unintuitive.

well, the uis wrapper you attached is mostly-fine-not-really code. it’s rather simple.

That aside, you are asking how to make code more systemic instead of hardcoding. In the Luau space, people make the common mistake of gravitating towards OOP. Don’t.
Functional. ECS. Anything else.
Hardcoding is fine until a project scales. At this point, split your scripts.

6 Likes

Can you elaborate on what you mean by this? Also, I’ve had a look at ECS before-and I never really understood how to implement this into luau.

All Luau OOP is fake OOP that costs runtime performance. ‘inheritance polymorphism encapsulation abstraction’. All of these are completely unnecessary and foreign to Luau, and their overheads add up.

ECS is simple (don’t use fake ECS like JECS)
wikipedia

Entity
An entity represents a general-purpose object. In a game engine context, for example, every coarse game object is represented as an entity. Usually, it only consists of a unique id. Implementations typically use a plain integer for this.[6]
Component
A component characterizes an entity as possessing a particular aspect, and holds the data needed to model that aspect. For example, every game object that can take damage might have a Health component associated with its entity. Implementations typically use structs, classes, or associative arrays.[6]
System
A system is a process that acts on all entities with the desired components. For example, a physics system may query for entities having mass, velocity and position components, and iterate over the results doing physics calculations on the set of components for each entity.

this is more fluff than it needs to be.
ECS → separating data from systems (functions, data-driven)
OOP → systems in data (classes, methods, inheritance)

OOP (methods + state bundled)

local Character = {}
Character.__index = Character

function Character.new(x, y, vx, vy, hp)
	return setmetatable({
		x = x,
		y = y,
		vx = vx,
		vy = vy,
		hp = hp,
	}, Character)
end

function Character:update(dt)
	self.x += self.vx * dt
	self.y += self.vy * dt
end

function Character:damage(d)
	self.hp -= d
end

local c = Character.new(0, 0, 10, 0, 100)
c:update(0.016)
c:damage(5)

ECS (data separated from systems)

local nextId = 0
local Position = {}
local Velocity = {}
local Health = {}

local function createEntity()
	nextId += 1
	return nextId
end

local function movementSystem(dt)
	for id, pos in Position do
		local vel = Velocity[id]
		if vel then
			pos.x += vel.x * dt
			pos.y += vel.y * dt
		end
	end
end

local function damageSystem(id, d)
	local h = Health[id]
	if h then
		h.hp -= d
	end
end

local e = createEntity()
Position[e] = { x = 0, y = 0 }
Velocity[e] = { x = 10, y = 0 }
Health[e] = { hp = 100 }

movementSystem(0.016)
damageSystem(e, 5)

The latter runs faster and pretends less. You don’t have to use either, but I advise against OOP.

5 Likes

Thank you for this, and I roughly do understand. However, do you have any tips or ideas on how to create this “mental model”? I just returned to coding after not touching studio since this summer, so I obviously am rusty, and maybe I just need to practice some stuff. But do you know what I can possibly work on or practice so I can start working on my mental model?

This is true, the interpreter has to do more work to handle OOP. However, there are applications of OOP where it makes things much easier to deal with. Windowing systems for instance. Each window is an object and everything related to that window is contained in the object that represents it. Mine uses something like 50+ different methods to control various aspects of a window and window content.

3 Likes

You’re not bad at coding. You just haven’t built the system thinking part yet. That wrapper is basically one central InputBegan and InputEnded listener that loops through registered actions and fires callbacks when a bind matches.

If you understand syntax, the next step is practicing small systems on purpose. Store data first using tables, then write one loop that handles everything. Copy patterns like this, rebuild them from scratch, and it will click over time.

If you want, I can share some scripts and systems I have used to learn. I pay about 200 USD a month for ChatGPT, so I have it deeply analyze my scripts, comment everything, and explain how it works. I can paste your scripts into it too and show you the breakdown.

I know AI can be frowned upon, but using it to help you learn instead of just having it make everything for you is invaluable. It is basically like having a teacher available 24 hours a day.

2 Likes

Looks like a basic input handler. For a simplistic version to learn from, it’s not bad. For something you would actually use, it’s kind of remaking the wheel. All this stuff is already in Lua. As far as how I personally feel about OOM, who cares. This is how I tackle new things: I just load up the links and study for as long as it takes to get a handle on them.

All about Object Oriented Programming
Efficient Object Oriented Programming Tutorial
A guide on creating classes in Lua
Concepts: Object Oriented Programming in Roblox
A look into Object Oriented Programming and performance
3 Different OOP approaches: performance, memory consumption, and aesthetics

This method is a bit hit or miss, but it’s bound to increase your knowledge of whatever subject you are looking to learn about. It will at least help ask more direct questions and do the same thing all over again.

1 Like

If you can speak English and your IQ is above 85, you can do this. The first step is to stop pretending that the mental model of a codebase is anything complex, especially in Roblox Luau. If you can code unassisted, you already do this.


This entire thread is somehow also uneducated. Shame! DO NOT USE OOP
I will defer you to another cultural movement in this regard:
https://devforum.roblox.com/t/the-new-luau-order-micro-optimizations-will-eat-all-ze-bugs/3969724
https://devforum.roblox.com/t/the-rise-of-the-new-luau-order/4116730

The original posts themselves contain insubstantial info, but the discourse below is good.
Did you read the thread that I attached?

anyway
@Yarik_superpro

2 Likes

Well then it might be my own problem-because especially for tasks that are above beginner-and more advanced levels-I struggle with coding unassisted. And what I mean by this is I always check for reference on what to do and plan out-and it often leads me to either using AI or searching the forums or watching a video-taking that-and adding my own to it. But it’s gotten me nowhere; and just makes me so used to having assistance. I realize now this was my greatest problem.

For now, this may be true.
Try what I suggested, and that will change over time.
You’ll get “punch drunk,” but you’ll even get better at that not happening as well.

1 Like

then stop thinking with systems. code happens frame-by-frame, it is not some continuous magical force.

in your case, we’re trying to wrap UIS for whatever reason. perhaps to manage binds. this involves:

  • exposing the functions to bind/unbind
  • handling the UIS connections underneath

just reason.
this isn’t a complex problem. you would never need a class for this. you are thinking too large for small problems. all this is is some unified registry for a input and a callback. start with not overcomplicating things

1 Like

That’s entirely separate logic, and Roblox has implemented style sheets for visuals. OOP stopped having any benefit with UI the moment Roblox added style sheets.
My player list UI, for example, is entirely functional programming oriented.
Unlike @A_Mp5’s method, though, mine uses a dictionary, but both are completely valid.

2 Likes

Ya, sometimes that is the best part.

3 Likes