Oberon_Command

Members
  • Content count

    4100
  • Joined

  • Last visited

Community Reputation

6087 Excellent

About Oberon_Command

  • Rank
    Contributor

Personal Information

  • Interests
    Design
    Programming
  1. Cache Coherency and Object Update

    The point is to prefer composition over inheritance. To me, that means that if you can do something without inheritance, and your code quality doesn't suffer for it, then you should. Composition rather than implementation inheritance is almost always a better way to achieve less coupled, more focused classes. It has been my personal and professional experience that the use of deep implementation inheritance has a tendency to lead to code that is brittle and difficult to maintain. Granted, I've spent 90% of my career on big legacy C++ codebases - but most newer code, even within those codebases, tends to use inheritance very sparingly rather than making it the centerpiece of the architecture because of how easily we know it can be misused. I'm talking something like 7-8 levels of inheritance on these old things which is downright pathological... Why does inheritance do that better than composition in this case? Why can't you just have a transform component that each of the components contains that does all that positioning stuff, without coupling the components' identities to the fact that they're transformable? You misunderstand; it's not a case of doing it "just to avoid inheritance," it's that composition is the default when designing new classes. From my point of view, most implementation inheritance is doing inheritance just to avoid composition! The purpose of inheritance isn't not to repeat code, it's to express "is-a" relations where an object must be treated as an object of its parent type. I suspect most modern OOP adherents would say that using inheritance to avoid repeating yourself is the wrong mindset entirely. I'm not even talking about cache coherency, I'm talking about architecture in general. And I have yet to see the internals of an engine that uses more than 1-2 levels of implementation inheritance that has not become complicated and bloated because of it.
  2. Cache Coherency and Object Update

    Because of Amdahl's law - you want your most common operations to be cache-friendly because you get the most overall decrease in latency by optimizing the parts of the program that take up most of its execution time. Practically, this tends to mean designing your data layout in such a way that your access patterns minimize cache misses. It's not so much that such operations are the most common, so much as you could structure things such that you make them the most common. I agree in principle, but OP is asking about cache coherency and how to structure his program in a way that's cache-friendly... It's not that they're reflecting inheritance, it's more that implementation inheritance is reflecting a special case of composition. Does a car have wheels, an engine, and a steering wheel, or is it a SteerableGasolinePoweredVehicle which is a GasolinePoweredVehicle which is a Vehicle? Suppose I want a drawable component that isn't positioned in the scene or even a skeletal mesh that isn't in the scene, but is instead a UI element drawn at a fixed screen position? Do I have to create a CSkeletalMeshComponentForUI class? What should that derive from? And why do I care that a CSkeletalMeshComponent can be placed in the scene if my code doesn't deal with the scene, anyway? Suppose I'm writing code that only cares about the mesh and shouldn't care where it is, or even that it's renderable? As I understand it, it's actually the current thinking in the object-oriented world at large to prefer composition over inheritance. Deep inheritance hierarchies are considered a bit of an anti-pattern nowadays. Most "is-a" relationships are better modelled as "has-a" relationships - a "CSkeletalMeshComponent has a transform" is more straightforward to understand, less coupled, and is less design restrictive than "CSkeletalMeshComponent is a drawable-compononent-with-a-transform." To go back a bit: I encourage you to try other models. Just because some major engines use this kind of hierarchical design doesn't mean you need to - or even should. Big engines tend to be big due to exposure and inertia and featuresets - you know, the business stuff - not because their gameplay architectures are superior.
  3. Cache Coherency and Object Update

    In my world, more than one level of inheritance is "deep inheritance." And I use implementation inheritance extremely rarely - interface inheritance happens more often, but is still only one level deep. If your inheritance is just about code sharing, you could implement more or less the same architecture using composition - instead of inheriting, you'd have your CSkeletalMeshComponent contain components of the previous types and delegate to them. You should only use inheritance if you need to be able to take a pointer to a CSkeletalMeshComponent and use it like it's a CSceneComponent instead. Do you really need that, or can you just register the CSceneComponent with a system that deals with those components without caring about what object contains it?
  4. Cache Coherency and Object Update

    Indeed. With my approach (and the ECS approach), there would also be no inheritance hierarchy, and components would be pure or nearly-pure data. Just based on that inheritance hierarchy, I'm assuming you store your components in heterogeneous containers in your game objects? That's going to be difficult to make cache-coherent, I think. I'm also wondering what a "component" represents in your system. Deep inheritance hierarchies on components seems unusual, given that an advantage of using components is getting away from deep inheritance hierarchies. What are you getting out of this decomposition?
  5. Cache Coherency and Object Update

    Well, for starters, this assumes that you keep your components in some kind of contiguous sparse array, where the components' "physical indices" don't need to be the same as their "logical indices" (ie. their entity identifiers), hereafter referred to as a "component container." The brute force way you could do that would be to have two entirely separate, hardcoded component containers. When you update the components, you'd run the same update loop on both containers, one right after the other. This is a bit off the top of my head, but suppose you could keep them in the same array, but just arrange the elements such that parents always come before children, and the rest of the ordering doesn't matter? So (for example), you might have the first 5 components be parent/childless components, and the next 3 be child components, and keep a "physical" index to the first child component. Re-parenting a component would just mean swapping components around. You make one of the parent components a child component by swapping the new child with the last parent, then decrementing the physical index to the first child component. You unparent a child component by swapping the child component with the first child, then incrementing the physical index to the first child component. This way, the parents and children still live in contiguous memory and you still have O(1) parent/unparent operations. Of course, this requires that your components be stored in contiguous memory even when there are deleted components, that they be able to be swapped in memory, and that you never take pointers to components.
  6. Cache Coherency and Object Update

    Sounds like a partitioning would be a better option than sorting, then. More lightweight than sorting AND you can do it in a way that clearly shows in the debugger which components are parents and which are children.
  7. Cache Coherency and Object Update

    My first question would be, why do you need parent before child updating at the component level in the first place? What's the use case for that? Remember, a "component" is ideally a bundle of state that, in the most common operation over that state, is contiguous in memory. Can you rethink your decomposition to avoid the issue? My second question would be, why can't you just sort the component array to enforce the update order? Or partition it, then update the partition with parents before the partition before children?
  8. Cache Coherency and Object Update

    If you're doing ECS "properly" (if such a thing can be said to be possible, given the disagreement over what ECS actually means) then you aren't updating each entity every frame, because an "entity" is just a binding mechanism that identifies groups of components. The most common operation should be updating homogeneous lists of components all at once, with operations that touch multiple components being comparatively "rare." Actually accomplishing this can indeed be "maddening" if you aren't used to thinking in those terms and don't have gameplay that's well-suited to that kind of decomposition. Of course, I would like to point out that you can use non-dogmatic, data-oriented "outboard composition" without locking yourself into dogmatic ECS...
  9. Really micromanaged audio programming (C++)

    DirectSound was also essentially deprecated as of 2011 and modern OSs may not support all of its features. XAudio2 was its replacement.
  10. Status Effects (Buffs Debuffs) in an ECS Architecture

    This. Instead of making your status effects "components", per se, just have a flat array for each type of active status effects and give each status effect a handle to an entity. Work otherwise as I described. Or, each status effect component could track a list of active status effects of that type: struct DamageReductionComponent { struct Record { float timeExpiration; int maxReductionPerHit; int totalRemainingReduction; } std::vector<Record> records; }; void update_damage_reduction(float currentGameTime, ComponentArray<DamageReductionComponent>& damageReduction) { damageReduction.erase_remove_if([currentGameTime](const DamageReductionComponent& component) { return std::all_of(begin(component.records), end(component.records), [currentGameTime](const DamageReductionComponent::Record& record) { return currentGameTime >= record.timeExpiration || record.totalRemainingReduction == 0; }); }); } void apply_damage_reduction(ComponentArray<DamageReductionComponent>& damageReduction, ComponentArray<SuccessfulHitComponent>& hits) { for (auto&& hitComponent : hits) { if (auto damageReductionComponent = damageReduction.try_and_get_mutable(hits.entity_of(hitComponent)) { for (auto&& reductionRecord : damageReductionComponent->records) { int damageToRemove = std::min(reductionRecord.maxReductionPerhit, hitComponent.damage); hitComponent.damage -= damageToRemove; reductionRecord.totalRemainingReduction -= damageToRemove; } } } } (note: I just woke up and wrote that from scratch, so it almost certainly has bugs, but it should get the idea across, the important thing is the way the data is structured)
  11. What are you trying to accomplish with this?
  12. Status Effects (Buffs Debuffs) in an ECS Architecture

    The example you mentioned it not ECS-like in the slightest and IMO is the wrong way to go about it even if you aren't working with an ECS-like architecture. The way I would do it is to have a component per status effect/buff. This component would contain any state the status effect needs, eg. a countdown until the effect expires or how much damage something like a "damage reduction" buff has absorbed. When a status effect is applied to an entity, that entity gets a component. The component is removed when the status effect expires or is otherwise removed. Any update logic specific to the status effect is applied to all entities with the status effect component. This is exactly the kind of thing that ECS is good at, provided your ECS allows you to think of "components" as small pieces of mostly self-contained state that can be added to and removed from entities at runtime. I don't think you can really get the benefits of ECS without supporting that kind of dynamic composition.
  13. That's... not ECS. That's an over-complicated way to do regular compile-time composition. I mean, what you're doing is pretty much exactly equivalent to this, albeit with some extra template boilerplate to access components by type (which you don't actually need if you're using static composition, you can just refer to the member directly): class Fighter { public: Fighter() = default; ~Fighter() = default; Comp::Transform transform; Comp::Velocity velocity; Comp::SpriteTileSheet spriteTileSheet; Comp::Animation animation; Comp::Input input; }; You still need to decide on component granularity, but if all the components are part of the same object (ie. you aren't using an architecture that supports "discontinuous composition", like ECS) certain design decisions become easier.
  14. You misunderstand. I'm not talking about accessing any entity from anywhere, I'm talking about accessing any component from anywhere, given an entity. In your last post you posted code that does this: entity.component<Position>() My argument is specifically against having any code like this. I'm saying that making arbitrary components accessible through the entity is "like" having global state, because there isn't any access control at the component level if you have the component's entity. By all means, write code that doesn't touch the entity itself. Even if you do your best to write code that doesn't touch the entity yourself, someone else working on your code will abuse this mechanism. Forcing them to declare their dependencies up front (or declare that they can depend on everything) discourages that kind of abuse. I speak from experience of what codebases turn into ten years of crunch later when given an object identifier you can access any part of its state. I am generally against the service locator pattern on similar grounds. You're right, I don't care that I have the component's type (although not having it can help discourage bad behaviour), I care that I can query any component state given an entity ID. With the code as you've written I can access any kind of component from anywhere that can access an entity. So then when I'm trying to follow code that takes an entity, I have to assume that it could access anything, even component state for which it doesn't include the header, since the code could access the component and pass it on to something else without touching it directly. It changes where we get the data from. With what you posted, the data is coming from one central (but obfuscated) location that's accessible to anything that has an entity - because the entity itself is the access point. With what I posted, the data is coming from the place where it is actually stored and has to be passed down explicitly. Having an entity doesn't give you access to its components unless the code calling your code says you can access the components. Well... yes, exactly! That's kind of the point. You now have to stop and add the dependency explicitly, forcing you to think about the dependencies you're adding and whether your code now has too many and needs to be refactored. Plus your successors will thank you for making the dependency obvious from just looking at the function signatures... Yes, that's exactly what I'm saying. I did use the phrase "component types," but I only meant that in the informal sense. Perhaps I should have been more precise. Indeed. In fact, I'm specifically advocating a approach to program structure which lets you take advantage of those things by allowing for cases where component symbols aren't even visible to code that doesn't deal with them directly. How does the Unity-style ECS where the entity is the access point for components allow you to do that? You seem to have gotten hung up on my use of the term "component types." The thrust of my point was that we shouldn't expose arbitrary component state to arbitrary code. The approach I suggested allows us to not expose the component types in some cases, as well, which enhances the effect but isn't the point.
  15. void move_character( Transform& transform, const CharacterDefinition& definition, const CharacterInputState& input, float dt) { const Vector desiredMoveDirection = input.get_desired_move_direction(); const Vector movement = definition.movementParameters.compute_translation_vector(desiredMoveDirection, dt); transform.translate(movement); } void move_all_characters( SparseArray<Transform>& transforms, const SparseArray<CharacterDefinition>& characters, const SparseArray<CharacterInputState>& inputStates, float dt) { for (const CharacterInputState& input : inputStates.get_active_set()) { const EntityHandle entity = input.get_handle(); const CharacterDefinition* definition = characters.try_and_get(entity); Transform* transform = transforms.try_and_get(entity); if (definition && transform) { move_character(*transform, *definition, input, dt); } } } Then we can call move_all_characters from a place where we have access to the transforms, characters and input states, which is probably glue code that binds all this stuff together. Of course, we don't actually need these to be sparse arrays, they could be some other kind of abstraction. Maybe even an opaque structure that lives off in memory somewhere and we don't actually know what it is, apart from the fact that we can call a function on it (or a reference or smart pointer pointing to it) that gives us a CharacterDefinition given an EntityHandle. This also allows you to have entirely different sets of entities for different kinds of "game objects" if some entities have entirely disjoint sets of components, rather than forcing all components to exist in the same set of entities. Of course, you can do that with other frameworks, too, but here that idea becomes very easy to implement.