What is component-projection in Untitled Mod Game?
"Component-projection" is a special feature that I use for my ECS in UMG.
It's (kinda?) similar to inheritance in OOP; except, it occurs on components, as opposed to objects.
At it's most basic level, component-projection is when one component forces another component to exist, by creating the target component automatically.
Example:
Lets say we have a generic drawable
component, that is attached to any entity that is drawable.
With this, we could have a few different components:
-- all things that can be drawn!
ent.image = "monkey.png"
ent.particles = Particles()
ent.circle = {radius=5}
With component-projection, we would have each of these components "project" to the drawable component:
How this works in practice, is that entities are automatically granted a drawable
component IF they have ANY of the listed components.
So when you do this:
ent.image = "bat.png"
It's effectively the same as doing:
ent.image = "bat.png"
ent.drawable = true
This probably smells very similar to OOP- which makes sense, because it is. Essentially, this is the exact same as an interface in OOP, but in reverse:
With an OOP interface, the interface FORCES methods/fields to be implemented.
With component-projection, the methods/fields FORCE the interface to exist.
It's the same thing, but in reverse.
(This is a bit off-topic, but in UMG, we can also use groups
as component-projection mediums; which allows us to project in more complex ways.)
That is, we can say stuff like:
- "If ent has
eyes
component ANDears
component ->"- "give entity
sensory
component"
- "give entity
Anyway, whats AWESOME about all this, is that we can now query entities without knowing about their particular implementation.
For example:
-- Draw System (pseudocode)
local drawGroup = umg.group("drawable", "x", "y")
umg.on("@draw", function() {
-- draw all entities!
for ent in drawGroup {
drawEntity(ent)
}
})
And internally, drawEntity
would emit some event through some event bus, like this:
function drawEntity(ent) {
-- dispatch `drawEntity` event
umg.call("drawEntity", ent)
}
-- Other systems listen for `drawEntity` event:
umg.on("drawEntity", function(ent) {
if ent.circle then
drawCircle(ent.x, ent.y, ent.circle)
end
})
umg.on("drawEntity", function(ent) {
if ent.image then
drawImage(ent.x, ent.y, ent.image)
end
})
What's also amazing about this setup, is that entities can have ALL of the above components, and they will work perfectly fine.
Is that not beautiful?
Taking it a step further- Values-projection:
We can take it a step further, by providing values for the projected component.
Example:
Here, we see nametag --> text
projection.
components.project("nametag", "text", function(ent)
local nametag = ent.nametag
-- this returned table will be the VALUE
return {
scale = SCALE,
default = DEFAULT,
component = "controller",
oy = nametag.oy or EXTRA_OY,
background = BACKGROUND_COLOR,
disableScaling = true
}
end)
Here is a code snippet yoinked straight from the umg ecosystem.
What's special, is that the projection is giving an actual value to the target component.
In this example, the nametag
component "passes" a bunch of default values to the text
component; most notably, it tells the text component to take the value of the controller
component; (which, notably, will be the username of the player.)
Pitfalls:
INCONSISTENCY: If multiple components project to the same component, and they both try to provide a value, only one will succeed. (This is kinda similar to the diamond-problem in OOP.)
MEMORY EFFICIENCY: For rcomp projection, its a bit inefficient, since we need to keep track of who is currently projecting to what component in the event of removal. This involves storing an entity in a SSet per projection.
COUPLING: Creates coupling between 2 components.
Thankz for readin!