Need to wait two physics frames when using GetOverlappingAreas sometimes

Godot Version

Godot4.5 Mono, 2d

Question

`
I implemented a design where enemies emit a signal when attacking or dying to create damage Area2Ds. After creation, I use GetOverlappingAreas to process overlapping objects, then destroy the area. However, I found it wasn’t detecting any overlaps.

After adding await ToSignal(GetTree(), SceneTree.SignalName.PhysicsFrame), the areas created on death started working properly, but the attack areas still didn’t work (no overlapping targets detected).

After various tests, I discovered that adding one more await (waiting for a second physics frame) made it work.

I’ve already switched to using onAreaEntered to solve the problem, but I’m curious why waiting for two frames was necessary.

Both cases use the same signal for triggering. Here’s the position triggering logic:
Attack:

	// 开始攻击
	private async void StartAttack()
	{
		if (_isAttacking) return;
		if (!IsTargetInAttackRange())
		{
			Log.Print($"敌人 {Name} 距离目标太远,进入移动状态");
			SetAttackTarget(Vector2.Zero, 0f);
			ResumeMovement();
			return;
		}
		_isAttacking = true;
		StopMovement();
		if (_animatedSprite.SpriteFrames.HasAnimation("Attack"))
		{
			_animatedSprite.Play("Attack");
			_currentAnimation = "Attack";
			await ToSignal(_animatedSprite, "animation_finished");
		}
		ExecuteAttack();
		_attackCooldownTimer.Start();
	}

Dying

	public async void Die()
	{
		if (_isDying) return;
		_isDying = true;

		// 2. 停止移动
		this._speed = 0;
		this.Velocity = Vector2.Zero;

		// 3. 禁用碰撞
		_collisionShape.SetDeferred("disabled", true);
		if (EnemyStats.DeathEffectArea != null)
		{
			CreateExplosionDamageArea();
			//爆炸素材是128*128的,所以需要根据爆炸素材的大小来缩放
			this.Scale = new Vector2(EnemyStats.DeathEffectArea.Size.X / 128f, EnemyStats.DeathEffectArea.Size.Y / 128f);
		}

		// 5. 播放死亡动画
		PlayAnimationIfNeeded("death");
		await ToSignal(_animatedSprite, "animation_finished");
		
		// 召唤单位不提供经验
		if (!IsSummoned)
		{
			GameEvents.Instance.EmitSignal(GameEvents.SignalName.EnemyDied, EnemyStats.Experience);
		}
		PressureDirector.Instance.UnregisterEnemy(this);
		// 6. 动画播放完毕后,安全地销毁自己
		this.QueueFree();
		
	}
  1. Please put your code between ``` on the lines above and below your code so it is easier to read,
  2. Your problem is related to the fact that you are trying to create something that detects collisions and are expecting it to do so the same frame it is created - which is cannot always do. If you use call_deferred() on the creation it might help, but I do not know how to do that in C#.

Thank you very much for your reply.
I tried using callDeferred, but it didn’t work. Later, through testing, I found that it only works by waiting for physics frames. The AI told me that Godot processes physics at the very end of the frame, so when the program executes, the physics information hasn’t been updated yet.

However, what confuses me the most is why, when attacking, I need to wait for two physics frames, whereas the death zone transmitted through the same signal only requires one.

Thanks for formatting your code.

It would help to see your node configuration to answer your question. If you could screenshot them it might help.

You are really kind and helpful. Thank you.
This diagram shows the overall structure, in which two of the three areas below are for attacking, and one is for when death occurs.


I can only attach one image, so let me describe it in words.
The enemy nodes are located under GameManagers → EnemySpawner → EnemyContainer.

They all use this line of code to emit the signal:

GameEvents.Instance.EmitSignal(GameEvents.SignalName.CreateDamageArea, attackData);

This signal is subscribed to by the DamageAreaManager, which then executes this response.

private void OnCreateDamageArea(DamageAreaData damageAreaData)
    {
        // 使用CallDeferred延迟添加子节点,避免在物理查询刷新期间修改Area2D状态
        CallDeferred(nameof(CreateDamageAreaDeferred), damageAreaData);
    }
    
    private void CreateDamageAreaDeferred(DamageAreaData damageAreaData)
    {
        DamageArea damageAreaInstance;
        // 动态创建TimedDamageArea节点
        if(damageAreaData.OneShot == false)
        {
            damageAreaInstance = new TimedDamageArea();
        }
        else
        {
            damageAreaInstance = new DamageArea();
        }
        
        // 初始化伤害区域
        damageAreaInstance.Initialize(damageAreaData);
        GetTree().CurrentScene.AddChild(damageAreaInstance);
        
        Log.Print($"伤害区域已动态创建并添加到场景: {damageAreaData.AreaType}");
    }

We trying to be helpful and welcoming here as a community. :slight_smile:

What’s the code for TimedDamageArea?

This is the code of DamageArea, They both use this one, because OneShot is True

using Godot;
using System.Collections.Generic;

//编辑器不应该操作此类型
public partial class DamageArea : Area2D
{
    protected DamageAreaData _data;
    protected HashSet<Node2D> _hitTargets = new HashSet<Node2D>(); // 记录已伤害的目标,避免重复伤害
    private Timer _durationTimer; // 持续时间定时器

    public override void _Ready()
    {
        // 连接信号
        BodyEntered += OnBodyEntered;
        AreaEntered += OnAreaEntered;
    }
    
    /// <summary>
    /// 启动持续时间定时器
    /// </summary>
    private void StartDurationTimer()
    {
        if (_durationTimer != null && IsInsideTree())
        {
            _durationTimer.Start();
        }
    }
    
    /// <summary>
    /// 持续时间结束回调
    /// </summary>
    private void OnDurationFinished()
    {
        Log.Print($"伤害区域持续时间结束,销毁区域");
        QueueFree();
    }

    public virtual void Initialize(DamageAreaData data)
    {
        _data = data;

        // 设置位置
        GlobalPosition = data.CenterPosition;

        // 设置碰撞层和遮罩
        CollisionLayer = data.CollisionLayer;
        CollisionMask = data.CollisionMask;

        // 根据区域类型设置碰撞形状
        SetupCollisionShape();
        
        // 创建并启动持续时间定时器
        _durationTimer = new Timer();
        _durationTimer.OneShot = true;
        _durationTimer.Timeout += OnDurationFinished;
        _durationTimer.WaitTime = data.Duration;
        AddChild(_durationTimer);
        // 使用CallDeferred确保定时器在场景树中后再启动
        CallDeferred(nameof(StartDurationTimer));
        
        Log.Print($"伤害区域已创建: 位置={data.CenterPosition}, 类型={data.AreaType}, 伤害={data.Damage}, 持续时间={data.Duration}秒");
    }

    protected virtual void SetupCollisionShape()
    {
        /*移除现有的碰撞形状,避免和编辑器配置冲突
        foreach (Node child in GetChildren())
        {
            if (child is CollisionShape2D)
            {
                child.QueueFree();
            }
        }*/

        CollisionShape2D collisionShape = new CollisionShape2D();
        AddChild(collisionShape);

        switch (_data.AreaType)
        {
            case DamageAreaType.Circle:
                CircleShape2D circleShape = new CircleShape2D();
                circleShape.Radius = _data.Size.X; // 使用X作为半径
                collisionShape.Shape = circleShape;
                break;

            case DamageAreaType.Rectangle:
                RectangleShape2D rectShape = new RectangleShape2D();
                rectShape.Size = _data.Size;
                collisionShape.Shape = rectShape;
                break;
        }
    }

    protected virtual void OnBodyEntered(Node2D body)
    {
        ProcessDamage(body);
    }

    protected virtual void OnAreaEntered(Area2D area)
    {
        ProcessDamage(area);
    }
    
    public override void _ExitTree()
    {
        // 清理信号连接
        BodyEntered -= OnBodyEntered;
        AreaEntered -= OnAreaEntered;
    }

    protected virtual void ProcessDamage(Node2D target)
    {
        // 检查目标是否已经受到过伤害
        if (_hitTargets.Contains(target))
        {
            return;
        }

        // 检查目标是否可受伤
        if (target is Injurable injurable)
        {
            // 计算击退方向
            Vector2 knockbackDirection = _data.KnockbackDirection;
            if (knockbackDirection == Vector2.Zero)
            {
                // 如果击退方向为零,则从中心向外
                knockbackDirection = (target.GlobalPosition - GlobalPosition).Normalized();
            }

            // 造成伤害
            injurable.TakeHit(_data.Damage, _data.KnockbackForce, knockbackDirection);

            // 记录已伤害的目标
            _hitTargets.Add(target);

            Log.Print($"伤害区域对 {target.Name} 造成 {_data.Damage} 点伤害");
        }
    }
}

Ok, so I think if you’re trying to do a death explosion type of effect, you’d be better off using a ShapeCast2D rather than an Area2D. This may solve your problem of multiple frames.

1 Like

OK, thank you. This indeed fits my needs better.
However, I’m still curious about why it takes two physics frames to obtain the physics information. Could it be that it fails to be included in the calculation during the first physics frame?
May I ask if there are any methods to directly access information from the physics engine?

TBH I’m not sure.

If you dig into RayCast and ShapeCast, there are ways to do this during the _physics_process() function.

I’m encountering this same issue. I need to wait two physics frames before recieving up to date overlap information from Area3D. I’ll also note that I’m using Jolt Physics.

I collected some information from the AI’s responses. I took a quick look and think it should be correct.
You can refer to this for a careful check. I don’t know much about Jolt

When having Area report collisions on its own, there will be a delay of several frames due to communication with the physics server. Godot’s recommended solution for this is to use direct_space_state or the ShapeCast2D node.

Jolt must work within Godot’s framework and therefore cannot bypass Godot’s PhysicsServer communication mechanism and inherent synchronization delays.

I’m glad you got some use out of AI, but you do realize I told you the exact same thing over 2 weeks ago?

Again using a RayCast2D/3D or ShapeCast2D/3D node allows you to process collision information every frame. Area2D/3D works just fine for most things, but if it doesn’t, you’re better off using one of those.

Thank you for continuing to follow this topic. However, you might have misunderstood my meaning.
The main purpose of my statement is to respond to Greeb — it seems that the latency of this method is unavoidable due to architectural issues — and to provide the suggested usage. My primary reason for consulting the AI is to find out the causes and supporting evidence.