sschmid / Entitas

Entitas is a super fast Entity Component System (ECS) Framework specifically made for C# and Unity
MIT License
7.13k stars 1.11k forks source link

If I ReplaceComponent many times in one frame, Reactive System only call once. How to fix this? #956

Closed atkdefender closed 3 years ago

atkdefender commented 3 years ago

Some "ReplaceComponent"s lose, does't call Reactive System.

WeslomPo commented 3 years ago

"If I ReplaceComponent many times in one frame, Reactive System only call once. How to fix this" - this is desired behaviour

sschmid commented 3 years ago

Reactive Systems are designed to aggregate changes until Execute is called. If you replace a component multiple times, that actually only triggers the system once and the latest data will be read once you call execute

sschmid commented 3 years ago
e.ReplacePosition3(new Vector3(0, 0, 0));
e.ReplacePosition3(new Vector3(1, 0, 0));
e.ReplacePosition3(new Vector3(2, 0, 0));

a reactive system with a trigger on Matcher.Position3 will collect the entity and when you call execute it and read the entity's position, you will read new Vector3(2, 0, 0)

atkdefender commented 3 years ago
e.ReplacePosition3(new Vector3(0, 0, 0));
e.ReplacePosition3(new Vector3(1, 0, 0));
e.ReplacePosition3(new Vector3(2, 0, 0));

a reactive system with a trigger on Matcher.Position3 will collect the entity and when you call execute it and read the entity's position, you will read new Vector3(2, 0, 0)

So that if I have some codes, need to react every time I call replacePosition, even in one frame. The codes should move out of the PositionReactiveSystem?

My original usage is AttackerEntity throw a DamageComponent to BehiterEntity. Then ReactiveSystem deals with the Trigger“damage.add()”. I should find another way to write this.

atkdefender commented 3 years ago

Reactive Systems are designed to aggregate changes until Execute is called. If you replace a component multiple times, that actually only triggers the system once and the latest data will be read once you call execute

I do some experiments to take it in.

  1. The ReactiveSystem.GetTrigger() collect what HAVE I triggered (Add(), Replace(), Remove()). (every frame once)
  2. The ReactiveSystem.Filter() check what is it NOW.
  3. If A_ReactiveSystem then B_ReactiveSystem, and B_ReactiveSystem.Execute() triggers A_ReactiveSystem.GetTrigger(), then A_ReactiveSystem will react next frame.

Anything I miss? I had read the wiki. https://github.com/sschmid/Entitas-CSharp/wiki/Systems

WeslomPo commented 3 years ago

My original usage is AttackerEntity throw a DamageComponent to BehiterEntity. Then ReactiveSystem deals with the Trigger“damage.add()”. I should find another way to write this.

This is not how you should do this. There are a problem that in one frame you can receive several damage entries, and you need to count all of them. Because, for example, your entity have 2 health, and it attacked by three different player entities - each deal 1 damage, you need to know who is deal latest damage before entity dies.

Right thing is doing this - make and component that have List of damages that dealed, and make a reactive system that react to changes of that list. And system will determine how much damage is dealed.

// Component
[Umpire]
public sealed class DamageComponent : IComponent {
    public List<ADamage> List;
    public static implicit operator List<ADamage>(DamageComponent component) => component.List;
    public override string ToString() => "Damage";
}

// AbstractDamage
public abstract class ADamage {
    public readonly EDamageType Type;

    public WeaponId AttackUid;
    public InstanceId Applicator;

    public float Power;
    public float Lifesteal;
    public bool IsCritical;

    protected ADamage(EDamageType type) { Type = type; }
    public abstract float Evaluate(Character target);
}

// Processing damage
public class DamageSystem : ReactiveSystem<UmpireEntity>, ICleanupSystem {
    private readonly IClock _clock;
    private readonly List<UmpireEntity> _clean = new List<UmpireEntity>();

    public DamageSystem(UmpireContext umpire, IClock clock) : base(umpire) {
        _clock = clock;
    }

    protected override ICollector<UmpireEntity> GetTrigger(IContext<UmpireEntity> context)
        => context.CreateCollector(
            UmpireMatcher.AllOf(
                UmpireMatcher.Character,
                UmpireMatcher.Damage,
                UmpireMatcher.AppliedDamage
            )
        );

    protected override bool Filter(UmpireEntity entity)
        => entity.hasDamage && entity.hasAppliedDamage && entity.hasCharacter && entity.damage.List.Count > 0;

    protected override void Execute(List<UmpireEntity> entities) {
        foreach (var entity in entities) {
            if (!entity.isDead)
                ProcessDamageList(entity);
            _clean.Add(entity);
        }
    }

    private void ProcessDamageList(UmpireEntity entity) {
        var character = entity.character.Reference;
        var shield = 0f;
        var health = entity.health.Value;
        var bubble = false;
        foreach (var damage in entity.damage.List) {
            var trueDamage = damage.Evaluate(character);

            if (ProcessShieldAndAfterItHaveAnyLeft(entity, ref shield, ref trueDamage))
                continue;

            bubble = true;

            ProcessDamageEntry(entity, damage, ref trueDamage, ref health);
            ProcessLifesteal(damage, trueDamage);

            // system that determine that entity is dead - next down execution order
            if (health.IsZero())
                break;
        }

        if (bubble)
            entity.ReplaceAppliedDamage(entity.appliedDamage.List);
    }

    private static bool ProcessShieldAndAfterItHaveAnyLeft(UmpireEntity entity, ref float shield, ref float trueDamage) {
        // to much code for example xD
    }

    private void ProcessDamageEntry(UmpireEntity entity, ADamage damage, ref float trueDamage, ref float health) {
        trueDamage = trueDamage.Min(health);
        entity.ApplyHealth(-trueDamage); // Shortcut extension - like entity.appliedHealth.List.Add(-trueDamage); entity.ReplaceAppliedHelath(entity.appliedHealth.List);
        // This lis holds all damages that received, and you can determine who kills who and who assists
        entity.appliedDamage.List.Add(
            new DamageApplied {
                Applicator = damage.Applicator.id.Value
                AppliedAt = _clock.Time,
                Damage = trueDamage,
                Type = damage.Type,
                IsCritical = damage.IsCritical
            }
        );
    }

    private static void ProcessLifesteal(ADamage damage, float trueDamage) {
        if (damage.Lifesteal > 0f)
            damage.Applicator.ApplyLifesteal(trueDamage * damage.Lifesteal);
    }

    public void Cleanup() {
        foreach (var entity in _clean)
            entity.damage.List.Clear();
        _clean.Clear();
    }
}
atkdefender commented 3 years ago

My original usage is AttackerEntity throw a DamageComponent to BehiterEntity. Then ReactiveSystem deals with the Trigger“damage.add()”. I should find another way to write this.

This is not how you should do this. There are a problem that in one frame you can receive several damage entries, and you need to count all of them. Because, for example, your entity have 2 health, and it attacked by three different player entities - each deal 1 damage, you need to know who is deal latest damage before entity dies.

Right thing is doing this - make and component that have List of damages that dealed, and make a reactive system that react to changes of that list. And system will determine how much damage is dealed.

// Component
[Umpire]
public sealed class DamageComponent : IComponent {
  public List<ADamage> List;
  public static implicit operator List<ADamage>(DamageComponent component) => component.List;
  public override string ToString() => "Damage";
}

Thank you so much for sharing your codes to me :D To memory a damage list instead of one damage component, clean it up at every end of the frame. It solved my problem. The “abstract” part also open up my eyes.