i have been using Lua (now via fennel) with LĂ–VE for a while, and i have found that one of the interesting and likeable things about Lua is how simple it is to implement mixins.
mixins are an odd, rarely used but convenient concept in OOP similar to interfaces and components but not really exactly like either. a mixin is a class that is used by other classes to absorb its methods without directly inheriting that class.
(local Enemy (Object:extend)) ; the base class
(local CanBeHurt (Object:extend)) ; the mixin class
(fn CanBeHurt:get-hurt [self damage hurter] ; a mixin method
(print (.. "owie i got hurt by" (tostring hurter)
" for " (tostring damage) " damage")))
(Enemy:implement CanBeHurt) ; implement the mixin on the base class
(local e (Enemy)) ; create an enemy
(e:get-hurt 100 "ivy sly")
; >> owie i got hurt by ivy sly for 100 damage
this implement class method (as implemented in the very good classic library) directly injects all of CanBeHurt’s methods to the Enemy class, such that all instances of Enemy can access the get-hurt method. If Enemy already has its own get-hurt method, the implement function will prioritize that existing method and quietly move on.
there are some interesting things you can do with this. i ended up treating them similarly to components. in my implementation, mixins can be applied at the object level as well as the class level, and they have a special init method that is treated essentially as extended construction behavior. for example, some Enemies could randomly spawn with a FireAura mixin implemented on the instance that initializes them with a self.fire-radius value, adds an update callback to search nearby players to damage and a few methods to help with this:
(local FireAura (Object:extend))
(fn Enemy.new [self]
(when (rng.percent 25)
; automatically call FireAura:mix-init on self with all parameters
(self:instance-mixin FireAura 50)))
(fn FireAura.mix-init [self radius]
(set self.fire-radius radius) ; set to 50
(self:add-update-callback
(fn [self]
(each [_ player (ipairs (self:get-nearby-players self.fire-radius))]
(self:fire-aura-attack player)))))
(fn FireAura.fire-aura-attack [self player]
(player:get-hurt 1 self))
when Enemy spawns, there is a 25% chance for that instance to implement the FireAura mixin, which calls FireAura’s mix-init method on the Enemy instance, and then injects all its methods (effectively just fire-aura-attack in this instance). what i like about this system is its convenience; all methods and values are top-level. the mixin is effectively invisible. you can access self.fire-radius directly from the Enemy instance, use the mixin methods and behaviors, all without typing FireAura all over the place. i’m not actually sure this is just a mixin anymore with all this extra behavior, but it’s effectively the same idea: inject the behavior from one class into another without dealing with complicated inheritance hierarchies. one could see now how this might be related to components, so let’s compare.
components, in contrast to mixins, are usually more visible, and less entangled with the object that uses them. a component is typically expressed as another data structure explicitly contained by an object. imagine an alternative implementation of FireAura:
(local FireAura (Object:extend))
(fn FireAura.new [self radius]
(set self.radius radius))
(fn FireAura.fire-aura-attack [self player]
(player:get-hurt 1 self))
(fn FireAura.update [self owner]
(each [_ player (ipairs (owner:get-nearby-players self.radius))]
(self:fire-aura-attack player)))
(fn Enemy.new [self]
(set self.components []) ; simple list of components
(when (rng.percent 25)
(self:add-component "fire-aura" (FireAura 50))))
(fn Enemy.add-component [self key comp]
; add it directly to our table so we can easily index it
; e.g. self.fire-aura.radius
(tset self key comp)
; also add it to the component list for easy iteration
(table.insert self.components comp))
(fn Enemy.update [self dt]
(each [i component (ipairs self.components)]
(when component.update
(component:update self))))
in the business of safe, clean code, this is probably superior to mixins most of the time, if requiring some extra work. but nobody said gamedev has to be safe or clean—would i be using Lua if i cared about silly things like type safety?—so what other benefit might object composition have over mixins? i think the obvious answer is that components are non-destructive. you can add a component to an object, then later remove it without pain. mixins tightly couple with classes that implement them, making them less flexible at the instance level.
but mixins are so convenient, aren’t they? i always optimize for laziness, that is, typing less, so i find the benefit of top-level field and method access hard to overstate. i am a big fan of duck typing. i’d much rather check for a simple obj.fire-radius number and ignore when it’s nil then have to go through the extra steps of checking something like (obj.fire-aura and obj.fire-aura.fire-radius). call me petty i guess. so here are some deranged ideas i have had about making components more mixin-like while remaining non-destructive.
option 1: unique field names
create a DSL to register every component with a name and the name of all of its fields, ensuring each field name is unique. something like:
; add a new component to the global component registry with all field and
; method names exposed for easy checking & removal
(defcomponent FireAura
field "fire-radius" 50 ; default value?
method "fire-aura-attack" (fn [self] ...)
...)
when you try to define a component with fields or methods that collide with the name of another component, it would error at compile time. doing this would let you apply components essentially as collections of string keys, and since there are no collisions, you wouldn’t need to contain them in separate structs.
(fn Enemy.new [self]
(when (rng.percent 25)
(self:add-component
FireAura
{"fire-radius" 60}) ; default override?
(print self.components.FireAura) ; >> true
(print self.fire-radius))) ; >> 60
this is obviously insane. the main issue with this is how brittle it is. what if there are non-component field names we need to watch out for? what about overriding component behaviors on specific instances? we also no longer get a major benefit of components, which is that components can themselves be objects, with all the nice things objects let you do. instead this turns them in to a loose bag of key-value pairs. and despite technically being less destructive, it doesn’t feel any better considering all the ways it could easily go wrong.
option 2: reduce declaration redundancy
a bit of a compromise. if we can front-load component access, then we can do roughly the same amount of typing, if a bit more. however i think we can do better than the following example:
(fn Enemy.some-method-that-uses-lots-of-components [self]
; `let` is just fennel's way of declaring multiple variables in
; sequence.
(let [comp1 self.comp1
comp2 self.comp2
comp3 self.comp3
comp4 self.comp4] ...))
i suspect we can cut this approximately in half with a hypothetical let-self macro that assumes self access.
(fn Enemy.some-method-that-uses-lots-of-components [self]
(let-self [comp1 comp2 comp3 comp4] ...))
this would compile to the same code as above. where it gets messy is combining other variable declarations…
(fn Enemy.some-method-that-uses-lots-of-components [self]
(let-self [comp1 comp2]
(let [derived (* 2 comp1.some-key)]
; assume for some reason we couldn't access these earlier:
(let-self [comp3 comp4] ...))))
suddenly we see more nesting and breaking up of variable declarations this way, making it hard to understand at first glance. maybe instead we could use a macro let& that works like normal let but allows opt-in self-keyed vars with a special identifier prefix:
(fn Enemy.some-method-that-uses-lots-of-components [self]
(let& [&comp1
&comp2
derived (* comp1.some-key 2)
&comp3
&comp4] ...))
this feels cleaner but we run into a couple issues. the first issue is i can never remember where & is on my keyboard and i have to look at it half the time lmao. typing symbols tends to slow me down a little. then there’s the problem of dealing with identifiers that actually have & in the name. it feels dirty and slightly limiting. it also isn’t especially readable at a glance unless you format it very clearly.
option 3: evil metatable bullshit
never do this, but what we could do is override __index on our Enemy object to automatically find component values:
(fn Enemy.__index [self key]
(var value nil)
(var found false)
; search through all our components
(each [i component (ipairs self.components) &until found]
(when (. component key)
(set found true)
(set value (. component key))))
(if (not= nil value)
; if a component with a value at that key exists, return that
value
; otherwise do a normal __index check
(. Enemy key)))
(fn Enemy.some-method-that-uses-lots-of-components [self]
; automatically finds the actual some-key in self.comp1
(let [derived (* self.some-key 2)]
...))
this is obviously horrible because adding another component with a conflicting field name can silently change behavior in ways that are hard to debug. there are no unique benefits i can think of to doing this that are not covered by other options. don’t do it
option 4: grow up
well yes, maybe i should just get used to a component based object structure and using my fingers to type a little more code. it’s not that annoying. just a little more annoying than what i’m already doing right now. but maybe being annoyed is good sometimes in a time when it is so easy to let my brain smooth over and let trillion dollar robots do my typing and my thinking for me.
