Hi
I need a hint.
Provided I have such construction
type Component = ref object of RootObj
type Entity = ref object
components = seq[Component]
method update(c: Component) = discard
Then, I have components, like:
type Fighter = ref object of Component
method update(c: Fighter) = discard
Finally
for c in entity.components:
c.update()
So it is quite common pattern. Collection of objects with common interface on which you perform some action. How can I preserve such functionality avoiding using methods (as I have heard core team do not like them).
usually the alternative is a closure. But in your case, I would use:
type Fighter = object
phys: PhysicsComponent
proc update(f: var Fighter) = update(f.phys)
type Flyer = object
phys: PhysicsComponent
proc update(f: var Flyer) = update(f.phys)
var
flyers: seq[Flyer]
fighers: seq[Fighter]
for f in flyers: f.update
for f in fighers: f.update
You can use templates and macros to fight the boilerplate. For example, you can use macros.getType in your update proc to iterate over the specific fields and call the component specific update procs.
The one you suggested would not be feasible as every entity can have various number of various components.
On the contrary, the traditional OOP-ish design you seem to favour is not "feasible". Hard to debug, hard to reason about, slow on the CPU as it's full of indirections.
What's missing from my solution is an ID mechanism to get some subtyping back. But an ID mechanism is very beneficial for serialization, networking and "weak" references too. For instance a unit can attack another unit (store its ID in the attack mode) but this unit can die in the meantime. With the ID mechanism you can check whether the unit still exists before decrementing its hitspoints, and if it doesn't exist anymore you use this knowledge to make the attacker perform some other action. You get all this correct behaviour for free and since the data is kept simple you might be able to parallelize the code too. Oh and an ID can be smaller than a pointer, so you also save space.
Please check out "data oriented design": http://www.dataorienteddesign.com/dodmain/node4.html
In fact, don't read what I wrote. Read this online book, it's superb, I cannot stop reading it... :-)
So, if I understand it right, when you have to update an object of a certain type, you generally have to do the same thing to all objects of that type. Thus, having the object store a pointer to its update function isn't very helpful, since you can just keep a table of all such objects that could be updated in that way. Polymorphism generally means that you have oversimplified your system, and made over-broad general statements, that could be better specified.
In the entity example for instance, if "fighters" and "flyers" both need to update the same way, they can share a common base update function, with no polymorphism. If "fighters" need to update in a different way from "flyers" then it's probably a bad idea to call both those two different operations "update".
type Fighter = ...
proc drive(f: var Fighter) = ...
type Flyer = ...
proc fly(f: var Flyer) = ...
var
flyers: seq[Flyer]
fighers: seq[Fighter]
for f in flyers: f.drive
for f in fighters: f.fly
If both have the same "PhysicsComponent" base, then you'd just say...
var
entities: seq[PhysicsComponent]
proc createFlyer(...) =
...
entities.add(self.phys)
proc createFighter(...) =
...
entities.add(self.phys)
...
for fighter in fighters:
fighter.drive()
for flyer in flyers:
flyer.fly()
for entity in entities:
entity.updatePhysics()
Each fighter and flyer has to be accessed twice this way, once as either drive/fly, and once as their general physical properties, but that's faster than accessing each entity once, and then having a pointer/enum to decide whether to update it in the "drive" sense, or the "fly" sense. You also need to store two pointers for every entity, one for "entities" and one for either "fighters" or "flyers", but with polymorphism you need to store a pointer to the custom update function anyway.
There would be potential space saved if you used enum style polymorphism, like...
for entity in entities:
case(entity.type)
of FIGHTER:
drive(entity)
of FLYER:
fly(entity)
else:
error("has to be a fighter or a flyer");
entity.updatePhysics()
Your "type" variable could be a single byte, or even a bit within some kind of "entity.state" bitfield, whereas with separate lists for fighters and flyers, at best they have to store a 2 byte integer to index a table of entities, and even then that limits the amount of entities you can have to 0xFFFF.
That's the only reason I could think to use polymorphism, is if you're so severely memory constrained that 4/8 bytes more per entity is a deal breaker.
The data oriented would be:
But again, if you don't mind performance, you barely gain anything.
So it exists but is not available yet.
which, for all practical purposes, is equivalent to its not existing at all. I am just cautious, nothing more.
In the meanwhile however, most of his ideas are appealing for high performance computing in general and I wonder how hard it would be to implement such an abstraction in Nim...
That said, it was not my intent to shamelessly hijack your thread: OOP is often nice to reason about and not so hard to debug depending on how you do it (just like functional programming), however it is true that it is full of indirections and generally slower that an FP equivalent. All I wanted to say is that OOP might not be doomed after all. :)
So it is quite common pattern. Collection of objects with common interface on which you perform some action. How can I preserve such functionality avoiding using methods
Uhm, did you mean for interface? Like discussed in this thread?
If you need to call some field/method that act accordingly when it's defined, I think closure is your best bet.
it's said "closure is poor man's method" ;)
But I personally prefer closure instead of method though