Components, Signals, and You: A guide to uncooking a spaghetti

How, what and why to code in BYOND.
Post Reply
User avatar
ninjanomnom
Code Maintainer
Joined: Wed May 31, 2017 3:35 am
Byond Username: Ninjanomnom
Github Username: ninjanomnom

Components, Signals, and You: A guide to uncooking a spaghetti

Post by ninjanomnom » #494358

This guide has been moved to https://hackmd.io/@tgstation/SignalsComponentsElements
It will be left up here in the state it was in for posterity but you should go to the updated version instead.

The datum component system (DCS) which was added in https://github.com/tgstation/tgstation/pull/29324 has been around for a while now and deserves a proper introduction and some explanation now that it's being more commonly used and expanded on. This has been a while coming, blame me for my laziness.

DCS is complicated. Not just because it uses clever solutions for complicated problems, the way you have to think about those problems changes drastically as well. Don't worry though, it's a bit of a pain to start but after the first learning period it just clicks. You see all sorts of terrible systems and code around the codebase that can be improved by extracting functionality into a more maintainable chunk. You realize features you can implement that are only viable when you can make the target of all that functionality abstract.

To name a few existing example, we have a component which handles an object making noise when hit/thrown/used/etc. You can apply this to anything, even walls if you want. A component which turns anything into a storage item. A component which handles rotating things by hand in character. A component that makes diseases spread. You get the point.

Let's get some terminology out of the way:
DCS, Datum component system: Bit of an outdated term now but this was the original name of the system as a whole
Components: Isolated holders of functionality. They hold all the data and logic necessary to accomplish some discrete behaviour.
Elements: The same thing as components but cheaper and more limited in functionality. Components will often be used in this guide to refer to both as a group.
Signals: The way components receive messages. Originally only components could receive signals but since then it has been expanded so anything can receive a signal if it's useful.

Signals and how they work
These are the primary way for components to interact with the rest of the game. They're fairly simple in concept: First you create a place the signal gets activated from. Let's have a signal that activates when someone walks over it. (I'm going to be using /datum/component/squeak for most examples in this post)

Code: Select all

#define COMSIG_MOVABLE_CROSSED "movable_crossed"

Code: Select all

/atom/movable/Crossed(atom/movable/AM, oldloc)
	SEND_SIGNAL(src, COMSIG_MOVABLE_CROSSED, AM)
Here we have the base Crossed proc set up to send a signal and do nothing else. src is given as an argument so that it is a signal with itself as the origin. Next up the signal type is given, it is defined in __DEFINES/components.dm then passed in to the signal. Functionally it's just a string acting as an identifier. The third argument in the signal is the movable that's crossing over, this gets passed to whoever is receiving the signal.

Now that we have a signal at the top of the inheritance chain, any other overrides of the Crossed proc will have to either call parent with ..() or send the same signal like we did here in order to make sure that signal is called whenever the proc is called. Avoid making duplicate signals as much as possible. Signals should have a single source of origin so it is as easy as possible to reason about behavior when receiving those signals. If you feel the need to make additional signal sources you should consider instead just making additional types of signals. More types of signals is free.

Now that this new signal exists we can make something able to listen for the signal.

Code: Select all

/datum/component/squeak/Initialize()
	[...]
	RegisterSignal(parent, COMSIG_MOVABLE_CROSSED, .proc/play_squeak_crossed)

Code: Select all

/datum/component/squeak/proc/play_squeak_crossed(datum/source, atom/movable/AM)
	[dostuff]
Here, when the object, a component in this case, is created, it registers for the signal. It is now "listening" for that signal and any time that signal is sent the component will know about it. Any arguments given in that signal get passed along to the listener. This then calls the proc specified when registering for the signal. You can make any proc get called this way, check out callbacks to see how that works.

Where did that datum/source come from? Signals will in addition to their normal arguments pass the originator of the signal as the first argument every time. This is useful when you have something listening for the same signal on multiple other objects.

You now have a working component receiving a signal from the object it's applied to, but what's a component?

Sealing away the bad code with components
Components are the original reason signals were added. Signals may have moved on to bigger things but components are still a very powerful tool for having complex functionality that is easy to maintain. Components are only in charge of themselves, they have, in general, simple responses to simple events. They don't care what else their owner is doing or how it's doing it. If the squeak component receives a signal saying that someone has walked on it's owner then by god it's going to play a sound.

As previously mentioned the primary way components interact with the world is through signals. There are a couple other ways though that I'll go over fast.

The component has a reference to the owner of that component. This is most commonly used to get some state information like the location of the owner to know where to play a sound etc.

The other way you can interact with components is through the GetComponent proc. This proc is plain and simple a crutch, what it does is allow you to get a given type of component that's been applied to an object. This method of interacting with a component hurts the entire usefulness of them. If you have a component that depends on external code to do functionality, you're moving back once more to spaghetti code. You cant know if you see all the possible paths logic can take just by looking at the component.

This bears repetition: The GetComponent proc is a crutch and hurts your component. Avoid it if at all possible.

Take a look at the following code. It is a component which poorly implements a way to modify the color of a bullet

Code: Select all

/type/path/bullet/proc/set_color(newColor)
	color = newColor
	var/datum/component/color/comp = GetComponent(/datum/component/color)
	if(comp)
		color += comp.newColor
Note the component isn't actually doing anything. It is not compartmentalizing anything. We're just using a component to hold a value for the sake of saying we're using a component.

There are many ways to do this properly, all of them involve signals:

Code: Select all

/type/path/bullet/proc/set_color(newColor)
	if(SEND_SIGNAL(src, COMSIG_BULLET_SETCOLOR, newColor) & COMPONENT_CANCEL_SETCOLOR)
		return
	color = newColor

Code: Select all

/datum/component/color/Initialize()
	RegisterSignal(parent, COMSIG_BULLET_SETCOLOR, .proc/set_color_react)

/datum/component/color/proc/set_color_react(type/path/bullet/source, newColor)
	source.color = newColor + red // It's pseudocode, go away
	return COMPONENT_CANCEL_SETCOLOR
First off we've removed the GetComponent, no longer is this object depending on that component existing. Everything in the bullet is generic and the component handles exceptional cases. Further, now that the functionality is isolated it would be very easy to instead change this to be able to modify color on any atom in the game, just move where the signal is and make sure it gets called and obeyed properly. There is a problem here with doing that, everything that overrides the proc will need to make sure as well that it has been canceled. A second solution to this problem makes it possible to fix that:

Code: Select all

/type/path/bullet/proc/set_color(newColor)
	SEND_SIGNAL(src, COMSIG_BULLET_SETCOLOR, args)
	color = newColor

Code: Select all

/datum/component/color/Initialize()
	RegisterSignal(parent, COMSIG_BULLET_SETCOLOR, .proc/set_color_react)

/datum/component/color/proc/set_color_react(type/path/bullet/source, list/arguments)
	arguments[2] += red // It's pseudocode, go away
You're probably wondering how this works; unless you're more experienced with byond and/or programming in general this is magic. Lists are passed as references, meaning if we modify it in one place, anywhere else that has that same list will also be modified. args is a special list in byond that is all the arguments of a proc, if we pass that list into a component, it is able to then modify that list which modifies the args of the proc that called it and all potential children of that proc. This is incredibly powerful. However you pay for it with a small loss in readability. Whether the situation calls for it is up to your discretion.

Keep the code for a particular feature as isolated in their respective components and you'll end up with a more maintainable, more extendable, and more bug resistant system. The costs in performance are minimal for all this and in many cases cheaper on the cpu. Memory wise there are potentially issues in extreme examples but there are plans in the works to make that less so.

Optimizing with elements
The short of it is elements are lightweight components. Only one instance per type of element exists and that single instance attaches itself to every object that makes use of that element. This is to allow for compartmentalizing behavior that isn't particularly viable in components due to memory constraints or for shared global state behavior.

Designing new components/elements
If you want to make a new component, stop and consider what it is exactly you're trying to accomplish. Components should generally be created as holders for behavior in isolation of how that behavior gets used. A wizard component including requirement for robes and such is probably not as good a component as a generic spellcaster component which gives something the ability to cast spells.

After deciding on the concept for your component you may want to consider an element instead if that component will be used heavily or is very simple. Components have more flexibility than elements but elements are very cheap to use. Don't worry too much if you can't decide and just make a component if you can't see how an element would work.

You may have found a component or element that does almost what you want so you want to work off of that, great! If it's a component don't make a subtype. Components have everything they need to have all behavior for all usecases in a single type. If you need another component you should probably split that component into multiple different components. If it's an element on the other hand this isn't quite true. You shouldn't be making subtypes of elements to add new behavior, that can easily all go in to one element if your scope is properly narrow. However, it is fine to make subtypes with different var values. For example let's say you want an element that applies a status effect to whoever consumes the food it is applied to. You could make different elements for the various subtypes.

If this is your first time making a new component you should now get some feedback from others regarding your planned design. Getting into the right mindset is hard for your first few components and others who are more experienced can help you with issues before they happen.
Narcissistic stuff others said/made for me
Spoiler:
Image
Image
User avatar
ninjanomnom
Code Maintainer
Joined: Wed May 31, 2017 3:35 am
Byond Username: Ninjanomnom
Github Username: ninjanomnom

Re: Components, Signals, and You: A guide to uncooking a spaghetti

Post by ninjanomnom » #494359

FAQ (Adding more as people ask questions that don't really fit in the above guide)

1. Why/How would I use signals on things other than components?
As mentioned in the above post, signals can be used on things other than components. More specifically they can be used on any datum which includes things such as mobs, items, turfs, etc. They work exactly the same as on components, just call RegisterSignal with the appropriate arguments for your use case. Why you would do such a thing depends on context; Some signals don't have an associated proc you can override for the same functionality, some signals are used in a proc you really don't want to override. The most common use case though is when you want to listen for a signal on another object in the game, for example a door listening for when something moves on to a nearby turf and opens preemptively.

2. Can I return [x] from a signal?
Only bitflags should be returned from signals. Not bools, not lists, and definitely not full blown objects. Return values are OR'd together and given to the original sender of the signal, to make it simple only bitflags should be returned through this so there isn't confusion over return values.

3. Do I need to clean up signals in Destroy?
All signal cleanup gets handled in /datum/Destroy() and doesn't need to be handled explicitly by anything which registers signals.

4. How cheap are [x]/What's the performance like for [x]?
Signals themselves are very cheap, comparable to proc call overhead. Registering signals is also very cheap and it is worth temporarily unregistering/reregistering if you'll be doing nothing with it rather than having a check in the receiving proc. Adding new components to things have measurable cost but as long as you're sensible with them it isn't a problem. In short: use them if it makes sense and don't worry about it too much. If you're working in performance critical code this can matter but if you're working with such code then I hope you know how to measure resource costs on your own.

I'll update the op with more if it seems necessary which it probably will be. I just got some motivation and spat this out based on previous conversations explaining these concepts to people.
Narcissistic stuff others said/made for me
Spoiler:
Image
Image
User avatar
Razharas
Joined: Tue Apr 15, 2014 8:55 pm
Byond Username: Razharas

Re: Components, Signals, and You: A guide to uncooking a spaghetti

Post by Razharas » #495126

This post really needs to be in some kind of coding wiki which we really should have had for many years already by this point

Otherwise itll just sink to obscurity of not-first page forever
User avatar
ninjanomnom
Code Maintainer
Joined: Wed May 31, 2017 3:35 am
Byond Username: Ninjanomnom
Github Username: ninjanomnom

Re: Components, Signals, and You: A guide to uncooking a spaghetti

Post by ninjanomnom » #495343

I mean I could stick it on the wiki I guess. This is more just so I can link it at people when I have to explain something for the nth time
Narcissistic stuff others said/made for me
Spoiler:
Image
Image
User avatar
ninjanomnom
Code Maintainer
Joined: Wed May 31, 2017 3:35 am
Byond Username: Ninjanomnom
Github Username: ninjanomnom

Re: Components, Signals, and You: A guide to uncooking a spaghetti

Post by ninjanomnom » #524114

Guide has been updated with some notes on elements, and on the creation of new components/elements
Narcissistic stuff others said/made for me
Spoiler:
Image
Image
User avatar
Tarchonvaagh
Joined: Wed May 01, 2019 9:30 pm
Byond Username: Tarchonvaagh

Re: Components, Signals, and You: A guide to uncooking a spaghetti

Post by Tarchonvaagh » #524185

Oh god
User avatar
Shaps-cloud
Code Maintainer
Joined: Thu Aug 14, 2014 4:25 am
Byond Username: Shaps

Re: Components, Signals, and You: A guide to uncooking a spaghetti

Post by Shaps-cloud » #524216

I feel like I have a good grasp on the uses for components and the many ways they can be put to use, but I'm less clear on where elements have their niche (partly because there's only a few of them so far). Do you have any systems in mind off the top of your head that would make good candidates for elementization?
P.S. Shoot Dr. Allen on sight and dissolve his body in acid. Don't burn it.
Image
User avatar
oranges
Code Maintainer
Joined: Tue Apr 15, 2014 9:16 pm
Byond Username: Optimumtact
Github Username: optimumtact
Location: #CHATSHITGETBANGED

Re: Components, Signals, and You: A guide to uncooking a spaghetti

Post by oranges » #524299

components: unit of behaviour, one component per atom, has specific state to that atom to implement it's behaviour, so only lives on one atom at a time

elements: unit of behaviour, has less or no state specific to the atom, so can handle the behaviour for many atoms at once, and save a bit of memory from not having a whole bunch of components that do the exact same behaviour.

A system that makes a good candidate for elementisation is one where there's no atom specific behaviour (consider say a generic knock element, that only plays a knock sound when a hit by signal is sent, it can register on many atoms to implement the behaviour, since there's nothing unique to each atom)
User avatar
ninjanomnom
Code Maintainer
Joined: Wed May 31, 2017 3:35 am
Byond Username: Ninjanomnom
Github Username: ninjanomnom

Re: Components, Signals, and You: A guide to uncooking a spaghetti

Post by ninjanomnom » #598819

This has been updated a bit and moved to https://hackmd.io/@tgstation/SignalsComponentsElements
Narcissistic stuff others said/made for me
Spoiler:
Image
Image
User avatar
Rohen_Tahir
Joined: Wed Jun 05, 2019 1:00 pm
Byond Username: Rohen Tahir
Location: Primary fool storage
Contact:

Re: Components, Signals, and You: A guide to uncooking a spaghetti

Post by Rohen_Tahir » #598851

neat
Image
Post Reply

Who is online

Users browsing this forum: No registered users