Given the player is spamming projectiles from a machine gun at hundreds of swarming enemies - I would like to find an optimal solution on how to pass damage from a projectile to an enemy and not make game suffer from performance issues.
I have discovered multiple options on how to accomplish it:
- Signals
- Direct method invoke
- Deferred method invoke
- Tweener
I have prepared the code that basically shows all 4 options in action (no testing done)..
Using Godot 4.0.2, C#
public partial class MyEnemy1 : Entity
{
[Signal]
public delegate void HitByBulletEventHandler(int damage);
public override void _Ready()
{
this.HitByBullet += OnHitByBullet;
}
public void HitByBullet(int damage) { }
}
public interface IHittableByBullet
{
void HitByBullet(int damage);
}
public partial class MyEnemy2 : Entity, IHittableByBullet
{
public void HitByBullet(int damage) { }
}
public partial class MyBullet : Area2D
{
public override void _Ready()
{
this.BodyEntered += OnBodyEntered;
}
private async void OnBodyEntered(Node node)
{
// option 1: signal
if (node is MyEnemy1 enemy)
{
enemy.EmitSignal(MyEnemy1.SignalName.HitByBullet, 1);
}
// option 2: direct invoke
if (node is IHittableByBullet target)
{
target.HitByBullet(1);
}
// option 3: deferred invoke
if (node is IHittableByBullet)
{
node.CallDeferred("HitByBullet", 1);
}
// option 4: tweener
if (node is IHittableByBullet target2)
{
var tweener = CreateTween();
tweener.TweenCallback(Callable.From(() => target2.HitByBullet(1)));
}
}
}
I would like to know the pros and cons of using these options.
For instance,
- direct method invoke - would halt the frame right away and wait the method finishes execution
- deferred method invoke - the damage to the enemy would only be done at the idle frame, after the current frame. Meaning there is going to be a slight delay before enemy receives the damage. Apart from it I am interested whether it affects the performance in any harmful way. Wouldn't this mean that there is no difference between direct invoke and this approach, if both methods freeze the game to some extent anyways. The only question is When it happens - immediately or at idle frame 🙂
- then the Signal and Tweener are two options I cannot comprehend. When do they execute the code and whether it's any different than calling the deferred method
- Signals are executed in the same order as they were added to the queue (FIFO)
- Signals are often brought up as a manageable way of communication between objects in
Godot. But I also think that interfaces just as convenient.. So I started to question myself if I want to use signals for every single case or would it be better to use deferred method invoke and the interface, for example.
If someone could correct me and provide some details on each approach - it would be very appreciated!