From 5411f6c14360e1a80097c2d18fb4b153ea1e77c6 Mon Sep 17 00:00:00 2001 From: Ben Ownby Date: Sat, 16 Nov 2024 08:25:42 -0500 Subject: [PATCH] edit pass 1 ECS for DM programers --- src/SUMMARY.md | 3 +- src/en/robust-toolbox/ECS/ecs.md | 6 + .../ECS/object-oriented-pitfalls.md | 131 ++++++++++++++++++ src/en/robust-toolbox/ECS/oop-rants.md | 127 ----------------- 4 files changed, 139 insertions(+), 128 deletions(-) create mode 100644 src/en/robust-toolbox/ECS/object-oriented-pitfalls.md delete mode 100644 src/en/robust-toolbox/ECS/oop-rants.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index b05b81462..ac276a1bb 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -61,10 +61,11 @@ Robust Toolbox ---------------------- - [ECS](en/robust-toolbox/ECS/ecs.md) + - [Object Oriented Pitfalls](en/robust-toolbox/ECS/object-oriented-pitfalls.md) - [ECS Events](en/robust-toolbox/ECS/ecs-events.md) - [ECS Full Example](en/robust-toolbox/ECS/ecs-full-example.md) - [ECS FAQ](en/robust-toolbox/ECS/ecs-faq.md) - - [OOP Rants](en/robust-toolbox/ECS/oop-rants.md) + - [Netcode]() - [Net Entities](en/robust-toolbox/netcode/net-entities.md) - [Connection Sequence](en/robust-toolbox/netcode/connection-sequence.md) diff --git a/src/en/robust-toolbox/ECS/ecs.md b/src/en/robust-toolbox/ECS/ecs.md index 7222adbcc..2a5661f56 100644 --- a/src/en/robust-toolbox/ECS/ecs.md +++ b/src/en/robust-toolbox/ECS/ecs.md @@ -4,6 +4,12 @@ RobustToolbox (RT) employs the Entity Component System (ECS) software architectu ECS is a fairly constrained pattern when compared to the entirety of object oriented programming. ECS does not need to utilize any object oriented concepts; however, RobustToolbox makes use of inheritance to ensure that programers do not need to build games with boiler plate. Inheritance is also used in the multiplayer configuration of RobustToolbox to alleviate the need for code repetition on the client and server. Beyond the engine's implementation of ECS and the multiplayer setups, programers should not consider inherence as a valid solution to an ECS problem. +```admonish info +For a solid, real example of ECS in action: [Stacks](https://github.com/space-wizards/space-station-14/pull/4046) and [Actors](https://github.com/space-wizards/RobustToolbox/pull/1774). + +These are external links to quality sources: [Entity Component System](https://en.wikipedia.org/wiki/Entity_component_system) +``` + ## Parts of ECS ```mermaid diff --git a/src/en/robust-toolbox/ECS/object-oriented-pitfalls.md b/src/en/robust-toolbox/ECS/object-oriented-pitfalls.md new file mode 100644 index 000000000..7b1aec165 --- /dev/null +++ b/src/en/robust-toolbox/ECS/object-oriented-pitfalls.md @@ -0,0 +1,131 @@ +# Object Oriented Pitfalls + +Many programers are familiar with object oriented programming (OOP) basics. Some of the contributors to SS14 and RobustToolbox have experience with BEYOND and the DM language which utilize unconstrained OOP as a platform for development. This guide exposes some of the pitfalls of OOP and how ECS and RobustToolbox avoid said pitfalls. + +## composition over inheritance + +Composition over inheritance is an object oriented design principal; ECS is considered to be a maximization of this principal. This document will demonstrate the what and why of Composition over inheritance and its relevance to RobustToolbox. + +### What is composition over inheritance? +In the case of a choice between inherence to grant properties and composition, fields which are other classes, which do the same, chose composition. The reasoning is that while inherence can be powerful, it is inflexible. + +### Why composition over inheritance for game objects? +Consider the following inheritance tree: + +```mermaid +flowchart TD + GameObject --> BaseDamageable + BaseDamageable --> BaseMob + BaseDamageable --> BaseItem + BaseDamageable --> BaseBuckleable + BaseDamageable --> BaseMachine + BaseMachine --> BasePoweredMachine --> CoffeeMaker + BaseItem --> Crowbar + BaseBuckleable --> Chair + BaseBuckleable --> Wheelchair + BaseMob --> BaseCarbon + BaseMob --> BaseSilicon + BaseCarbon --> Human + BaseCarbon --> Cow + BaseCarbon --> Horse + BaseSilicon --> Borg + BaseSilicon --> AI + BaseSilicon --> pAI + Human --> Monkey + Human --> Catgirl +``` + +This tree is highly efficient, all code that would need to be repeated is instead inherited from a parent class, but manipulating this tree is cumbersome as the placement of a new type of game object must be carefully considered both for the present and future. + +1. Borgs for example which are neither AI nor PAI, would need to draw power from the powernet like `BasePoweredMachine`. In this tree Borgs could inherit from `BasePoweredMachine`, but that is not an option as borg *must* inherit from `BaseMob` and `BaseSilicon`. `BaseMob` could be moved to inherit from `BasePoweredMachine`, but this would grant unwanted functionality to `BaseCarbon`. The only effective option is to replicate the code across `BasePoweredMachine` and `Borg`. + +2. Another example is allowing players to buckle themselves to only horses and borgs. Moving `BaseMob` to inherit from `BaseBuckleable` once again will not work as the buckling properties will transfer to mobs which should not be buckled to. Code replication is again required in this instance. + +Code replication should be avoided as it introduces the issue of keeping replicated code artifacts in sync. The strategy that ECS uses to avoid these inheritance problems is to grant properties to game objects. A game object, instead of a member of a tree, is defined by the properties (components), that it is *composed* of. + +```mermaid +flowchart TD + subgraph Human Mob + Damageable; + Buckleable; + PlayerControllable; + end +``` + +```mermaid +flowchart TD + subgraph Borg Mob + Damageable; + PlayerControllable; + Buckle; + PowerReceiver; + end +``` + +```mermaid +flowchart TD + subgraph Crowbar Item + Item; + Weapon; + Tool; + end +``` + +```mermaid +flowchart TD + subgraph Chair Entity + Damageable; + Buckle; + end +``` + +```mermaid +flowchart TD + subgraph Coffee Maker Machine + Damageable; + PowerReceiver; + SolutionContainer; + SolutionProducer; + end +``` + +By using composition the problems from before are solved neatly, without duplicating any code and allowing for better maintainability and extensibility: +- Borgs and coffee makers share the `PowerReceiver` component, which allows both to draw power from a net. +- Borgs and chairs share the `Buckle` component, which allows entities with `Buckleable`, like humans, to be buckled to them. +- Humans and borgs share the `PlayerControllable` (also known as `Mind` in SS14 code) component, which allows them to be controlled by a player. +- And most of the game objects above share `Damageable`, which allows them to have "health" and be damaged. + +## Encapsulation + +Encapsulation is the fundamental concept of OOP. + +### What is encapsulation? + +Encapsulation is the bundling of data and functions/logic. These bundled functions are called methods and operate on the data within an object. Both the data and methods can be shown or hidden to outside classes. + +## why not encapsulation? +The bundling of data and logic raises an issue. Game code is highly coupled, so idea of game objects containing properties (components) is discarded by encapsulation. Any methods written for these components must also know about and interact with other components. A borg with the `PowerReceiver` component and `PlayerControllable` component may have changes to control when power is low. The `PlayerControllable` component would need to know about the `PowerReceiver` component to change the player speed, and the `PowerReceiver` component would need to know about the `PlayerControllable` component to deplete the internal battery. + +Either methods must be added to components to communicate between themselves causing high coupling, or the game logic should be moved out of components. ECS and RobustToolbox do not combine game data (components) and the logic required to operate said component into the same bundle. Instead logic is moved into systems which to avoid the same problem solved by moving logic out of components do not contain state. + +Systems have the ability to operate on more than one component simultaneously. For example a `PoweredMovementSystem` could operate on all `PowerReceiver` and `PlayerControllable` components contained within a single entity. Entities witch contain those components will be operated on by the `PoweredMovementSystem`. Multiple systems can operate on components at the same time. Input being taken by `PlayerInputSystem` and `PoweredMovementSystem` can both operate on `PlayerControllable`. + +Because systems are external to components and still need to interact, components must only contain public fields. This allows for direct interaction with components from any system. Each system could interact with every component, but this would likely cause redundant code. Systems may call functions on other systems to avoid replication functionality that always exists. Since there is no state in a system there should be no concern of side effects. However, the order in which systems are executed will change the outcome and should be configured. + + +## Conclusions +1. The game world has a bunch of entities, the entities have components + + Entities aggregate components to form game objects + +2. There are systems that operate on components + + Systems should give the entities their behavior, when they have the appropriate components. + +3. Components have no logic + + Components should only have data in them, and no logic whatsoever. + +4. Systems have no state + + Systems should only have logic in them, and no data whatsoever. diff --git a/src/en/robust-toolbox/ECS/oop-rants.md b/src/en/robust-toolbox/ECS/oop-rants.md deleted file mode 100644 index 31376a6fc..000000000 --- a/src/en/robust-toolbox/ECS/oop-rants.md +++ /dev/null @@ -1,127 +0,0 @@ -# OOP Rants - -This is the rant that acted as the preamble to old ECS documentation. - -## The solution to 'OOP is Bad': ECS - -So, the ECS ([Entity Component System](https://en.wikipedia.org/wiki/Entity_component_system)) gang has finally convinced you that OOP is bad and ECS is good, huh? -Good! OOP is indeed bad. In this document, we'll revisit some basic concepts such as Components, Entity Systems and Events, and explore an ECS approach to them. - -```admonish info -For a solid, real example of ECS in action on our codebase, take a look at [Stacks](https://github.com/space-wizards/space-station-14/pull/4046) and [Actors](https://github.com/space-wizards/RobustToolbox/pull/1774). -``` - -## Why composition over inheritance for game objects? -When you think of how to design game objects such as humans, items or walls, your first idea might be to use complex inheritance trees for this: - -```mermaid -flowchart TD - GameObject --> BaseDamageable - BaseDamageable --> BaseMob - BaseDamageable --> BaseItem - BaseDamageable --> BaseBuckleable - BaseDamageable --> BaseMachine - BaseMachine --> BasePoweredMachine --> CoffeeMaker - BaseItem --> Crowbar - BaseBuckleable --> Chair - BaseBuckleable --> Wheelchair - BaseMob --> BaseCarbon - BaseMob --> BaseSilicon - BaseCarbon --> Human - BaseCarbon --> Cow - BaseCarbon --> Horse - BaseSilicon --> Borg - BaseSilicon --> AI - BaseSilicon --> pAI - Human --> Monkey - Human --> Catgirl -``` - -Seems fine at first, right? -However, as you keep adding more and more features you'll soon realize the limitations of this. Let's go over some of the problems you might encounter. - -1. Say you wanted to make borgs, but not AIs nor pAIs, draw power from the powernet like `BasePoweredMachine` can. -What do you do? Do you make borgs inherit from `BasePoweredMachine`? -That's not an option, as they need to inherit both `BaseMob` and `BaseSilicon`. -Do you make `BaseMob` inherit `BasePoweredMachine` instead? -That doesn't make sense either, most mobs will not need that functionality. -Your only option here is to duplicate the code that handles power across `BaseMachinePowered` and `Borg`. -2. Now, say you wanted to allow people to buckle themselves to horses and borgs, but not the rest of mobs. -What do you do, do you make `BaseMob` inherit `BaseBuckleable`? -That doesn't make sense, as most mobs will not need that functionality. -Your only option again is to duplicate code across multiple, distant classes. - -As we've seen, having complex inheritance trees like this is not ideal, and eventually forces us to needlessly duplicate code, or give certain game objects functionality they're never gonna need. -The solution to all these problems is to use **composition** instead. - -```mermaid -flowchart TD - subgraph Human Mob - Damageable; - Buckleable; - PlayerControllable; - end -``` - -```mermaid -flowchart TD - subgraph Borg Mob - Damageable; - PlayerControllable; - Buckle; - PowerReceiver; - end -``` - -```mermaid -flowchart TD - subgraph Crowbar Item - Item; - Weapon; - Tool; - end -``` - -```mermaid -flowchart TD - subgraph Chair Entity - Damageable; - Buckle; - end -``` - -```mermaid -flowchart TD - subgraph Coffee Maker Machine - Damageable; - PowerReceiver; - SolutionContainer; - SolutionProducer; - end -``` - -As you can see, by using composition all our problems from before are solved neatly, without duplicating any code and allowing for better maintainability and extensability: -- Borgs and coffee makers share the `PowerReceiver` component, which makes them able to draw power from a net. -- Borgs and chairs share the `Buckle` component, which allows entities with `Buckleable`, like humans, to be buckled to them. -- Humans and borgs share the `PlayerControllable` (also known as `Mind` in SS14 code) component, which allows them to be controlled by a player... -- And most of the game objects above share `Damageable`, which allows them to have "health" and be damaged. - -TODO: finish this ----- - -Edit all this text into something proper for a doc... Sigh - -Okay so we usually mean a bunch of things by "OOP bad", and I'm not sure how to condense it all into a simple explanation but here I go. -1. First of all, inheritance. There are many problems and inflexibility that come with complex inheritance trees... Imagine we had no components and instead had a big-ass inheritance tree: If you had a "machine" base class that has power consumption, and a different "mob" base class that has funny player-controlling mechanics, now you essentially cannot make a mob have machine qualities or vice versa without making ugly hacks, or a common base for both "machine" and "mob". But at the same time, that wouldn't make much sense either! Most mobs aren't gonna need "power consumption features", and few machines are gonna be player-controlled at all... So to solve this awful, gnarly problem we use "composition" (components!) instead of inheritance. You know this stuff already, if you want an entity to have hands, add `HandsComponent`. If you want to make it consume power, `PowerConsumerComponent` will help! If you want to make it player-controllable, add `MindComponent` and control the entity and-- oh hey we made a "cyborg mob" out of reusable, generic components! This, of course, is hard to do with a big-ass inheritance tree alone... Of course, inheritance can be good and fine for small things, or when you have a very small and self-contained inheritance tree (see something like `SoundSpecifier` for example, it's tiny but inheritance helps a ton there) but when you have a big complex game like ss14, inheritance just makes things way more painful. -2. Also, encapsulation is another one. OOP likes to put both data and methods/logic on the same class, as a bundle, and only expose certain things to the outside of that class. You know, the funny access modifiers like `public`, `private` and such? So encapsulation is good for something like the engine, which specifically needs to obscure/prohibit access to some data or methods. But it doesn't make thaaaat much sense for game data and logic, for example. `StackComponent` in SS14 have no private fields or properties in 'em, anyone is free to go and read/write the values as they please. However, this is not the recommended way to interact with stacks, at all! -`StackSystem` has a few methods to operate on `StackComponent`, and change its values. So to use a certain amount of things on the stack, you use `StackSystem.Use`, to split it you use `StackSystem.Split`, etc, and `StackSystem` will take care of everything for you. Because turns out that changing the stack amount value isn't enough. You also need to do funny stuff such as: dirtying the component for network syncing purposes, setting the appearance value, raise a `StackCountChanged` event, etc. -The way you would do this in E/C would be to put all of that logic in the "amount" property on `StackComponent`, or maybe a method. Then you could `private` the actual "amount" number away. However the E/C architecture will not be accepted in this codebase, we will only use the ECS architecture going forward. - -See, putting any kind of logic in a component class doesn't make sense at all. If we think about how stuff in ECS is structured, it's like this: -1. Our game world has a bunch of entities, the entities have components -2. There are systems that operate on components - -Therefore, components should only have data in them, and no logic whatsoever. Systems should give the entities their behavior, when they have the appropriate components. -I'm more saying like... Rather than have logic in components change stuff, we want entity systems to operate and modify components. So instead of having: -`HandsComponent.Pickup(ItemComponent)`, you would have: -`HandsSystem.Pickup(UserEntity, ItemEntity)`.