first draft for On ECS!~

This commit is contained in:
Rowan 2024-12-20 17:42:37 -06:00
parent 30a5490703
commit a2e2c463f1

View file

@ -293,7 +293,7 @@ One of the other nice things about `SystemAPI.Query` is that it returns an enume
Other than being more convenient, there are significant benefits to querying entities this way that will be covered in just a moment. Other than being more convenient, there are significant benefits to querying entities this way that will be covered in just a moment.
## Clockwork ## Inexorable
Given the range of implementations and libraries, there are also some other patterns that have emerged as somewhat standard. Queries are present in nearly every library I've used, but there's a couple other useful concepts. Given the range of implementations and libraries, there are also some other patterns that have emerged as somewhat standard. Queries are present in nearly every library I've used, but there's a couple other useful concepts.
@ -313,12 +313,73 @@ Most of the performance benefits of ECS are due to its overlap with data-oriente
## What comes around is all around ## What comes around is all around
Now that our tools have been introduced, let's see if they're applicable to the three scenarios we laid out earlier.
### Spiked Shield ### Spiked Shield
Rather than modeling a hierarchy of inheritance, let's try handling our items with components instead. We can do something similar to the interface-based approach since that's almost exactly how ECS works anyway. I'll go back to using a fake ECS syntax here for simplicity.
```cs
struct Damage {
public float Value;
}
struct Defense {
public float Value;
}
var sword = Entity.create()
.withComponents(new Damage { Value: 25 });
var helmet = Entity.create()
.withComponents(new Defense { Value: 5 });
var spikedShield = Entity.create()
.withComponents(new Damage { Value: 10 }, new Defense { Value: 10 });
```
All we have to do is determine required components for handling damage and add those. The identity of the spiked shield is determined by its component composition.
### Damage ordering ### Damage ordering
Since our systems are handled in one place in a well-defined order, this shouldn't be an issue unless we go out of our way to make it one.
I'll use Bevy as an example for system ordering.
```rs
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Update, (
detect_damage_collisions,
apply_damage.after(detect_damage_collisions)
)
.run();
)
```
Not all libraries allow ordering in this way, but they should *all* have ways to establish some order between systems.
### Health and UI ### Health and UI
To accompish the logical separation between game state and its representation, all that really needs to happen is for our health UI system to query for the entities that it cares about an then render those elements.
```cs
// we're going back to our imaginary C# ECS library
public void UpdateHealthbarSystem() {
// let Player be a tag component
var playerHealthQuery = Query<Health, Player>();
var healthbarsQuery = Query<Target, Healthbar>();
foreach(var (target, healthbar) in healthbarsQuery) {
if (playerHealthQuery.HasComponent(target.entity)) {
// throw away the Player tag component
var (health, _) = playerHealthQuery[target.entity];
healthbar.value = health.current;
}
}
}
```
Did I cherry pick these examples to highlight the strengths of ECS? Kind of, but it's not like there are any Did I cherry pick these examples to highlight the strengths of ECS? Kind of, but it's not like there are any
## Weaknesses ## Weaknesses
@ -340,29 +401,25 @@ This example will probably be a little contrived, but let's take a damage system
What would the system for applying damage to health look like? What would the system for applying damage to health look like?
```cs ```cs
public void ApplyDamageSystem(ref SystemState state) { public void ApplyDamageSystem() {
// this is an object which can alter the state of the game world // this is an object which can alter the state of the game world
// eg. adding and removing entities or components // eg. adding and removing entities or components
EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob); var commands = new CommandBuffer();
// this is effectively a query for every entity with a Health component var healthQuery = Query<Health>();
var allHealth = GetComponentDataFromEntity<Health>(false); // the bool argument is whether its read-only
foreach(var (damage, entity) in SystemAPI.Query<RefRO<Damage>>().WithEntityAccess()) { foreach(var (damage, entity) in Query<Entity, Damage>()) {
if(!allHealth.HasComponent(damage.target)) { if(!healthQuery.HasComponent(damage.target)) {
return; // return if our target doesn't have health return; // return if our target doesn't have health
} }
var targetHealth = allHealth[damage.target].Value; var targetHealth = healthQuery[damage.target].Value;
var newHealth = targetHealth.current - damage.value; var newHealth = targetHealth.current - damage.value;
targetHealth.current = Mathf.Max(newHealth, 0); targetHealth.current = Mathf.Max(newHealth, 0);
// destroy the entity so it isn't processed on the next frame too // destroy the entity so it isn't processed on the next frame too
ecb.DestroyEntity(entity); commands.DestroyEntity(entity);
} }
ecb.Playback(EntityManager);
ecb.Dispose();
} }
``` ```
@ -385,7 +442,7 @@ Could we do this differently?
Instead of creating a separate entity for damage, we could add the component directly to the damaged entity. With this approach, other sources of damage will have to make sure to check for an existing instance of damage and modify that one if it exists, otherwise add a new one. This eliminates our join but increases the complexity of adding and removing multiple sources of damage to a single target. Instead of creating a separate entity for damage, we could add the component directly to the damaged entity. With this approach, other sources of damage will have to make sure to check for an existing instance of damage and modify that one if it exists, otherwise add a new one. This eliminates our join but increases the complexity of adding and removing multiple sources of damage to a single target.
This is actually a pretty simple query too. It can get complicated quickly though I'm sure some of this is my own inexperience. This is actually a pretty simple query too. It can get complicated quickly as more joins are needed, though I'm sure some of this is my own inexperience.
## Anyway ## Anyway