godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
88.62k stars 20.09k forks source link

Godot 4 - CharacterBody2D does not actively detect Area2D (regressed from Godot 3.5.3) #84511

Open fuzhouch opened 10 months ago

fuzhouch commented 10 months ago

Godot version

4.1.2.stable.flathub [399c9dc39] (regressed: v3.5.3.stable.flathub [6c814135b])

System information

Godot v4.1.2.stable (399c9dc39) - Freedesktop SDK 23.08 (Flatpak runtime) - Wayland - Vulkan (Compatibility) - Mesa Intel(R) Xe Graphics (TGL GT2) () - 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz (8 Threads)

Issue description

I noticed an inconsistent behavior between CharacterBody2D (Godot 4) and KinematicBody2D (Godot 3).

In Godot 3, KinematicBody2D actively detects an Area2D, if Area2D sets a collision bit and KinematicBody2D sets a corresponding mask. The detection can only be prevented by setting Area2D.monitoring = false (Note: it's not .monitable property.).

The behavior is different in Godot 4, that when Area2D sets a collision bit and CharacterBody2D sets a corresponding mask, the detection does not happen anymore.

Note before continuing

This issue can have two interpretations, that a) it's a designd change that Godot 4 intentionally prevent CharacterBody2D detecting Area2D, just didn't explain in documentation, or b) it can be a Godot 3 quirk because .monitoring property does not seems to affect this behavior.

Given there's a clear workaround (described below), it should be a low priority issue, but a confirmation of expected behavior is needed.

Use case scenario

I've developed an action game with a reactivatable enemy. The enemy supports two behaviors:

In Godot 3, this feature is implemented by settings collision bits. A brief setup looks like below:

With setup above, player does not collide with a defeated enemy (it has no collision layer bit). The defeated enemy responds to flame (KinematicBody2D sets mask bit while Area2D sets layer bit). Requirements are met.

However, when migrating my game to Godot 4 with same setup, the defeated enemy does not respond to flame. When I calls $CharacterBody2D.set_collision_layer_value(ENEMY, true) and $Area2D.set_collision_mask_value(ENEMY, true), the defeated enemy responds to both flame and player. Neither is expected.

Workaround in Godot 4

A workaround is to add a new collision layer, DEFEATED_ENEMY.

When an enemy is defeated, player does not collide with enemy (because player collides with ENEMY but not DEFEATED_ENEMY), while flame can reactivate enemy (because it detects DEFEATED_ENEMY).

Side effect of the workaround: The layer/mask bits are limited to 32 slots. It needs to be shared between terrain, characters and players. Adding more complicated interactions for enemies with different skills can exhaust the slots.

Steps to reproduce

Use my minimal comparison project repository. https://github.com/fuzhouch/godot-3to4-comparison/

  1. Use Godot 3.5.3 to launch Godot 3 project, godot3
  2. Run scene, Demo_Area2D_Detection_Object_Layer_Mask_Match.tscn
  3. Use Godot 4.1.2 to launch Godot 4 project, godot4
  4. Run scene, demo_area2d_detection_object_layer_mask_match.tscn.

Both scenes define two Area2Ds, a Blue area and a Red area. Every area is equipted with a corresponding character (a big gold). Every gold repeats 2-sec cycle of moving into the area, then moving out. Every area has a body_entered signal handler, printing a line on called.

Blue area represents a good case (area-detect-body mode), that characeter sets collision layer as PLAYER (value == 1), and area sets collision mask as PLAYER (values == 1). Red area represents a bug-reproduce case (body-detect-area mode), that character sets collision mask as EFFECT (value == 4), and area sets collision layer as EFFECT(value == 4).

Expected results

When running scene, we expect both Godot 3 and Godot 4 prints 2 lines every 2 seconds:

detector area Blue entered: Character_LayerSet_MaskUnset
detector area Red entered: Character_LayerUnset_MaskSet
...

Actual results

Only Godot 3 prints 2 lines every 2 seconds

detector area Blue entered: Character_LayerSet_MaskUnset
detector area Red entered: Character_LayerUnset_MaskSet
...

2023-11-06 17-40-39

In Godot 4, it prints 1 line every 2 seconds. The Red area do not trigger callback.

detector area Blue entered: Character_LayerSet_MaskUnset
...

2023-11-06 17-37-46

Minimal reproduction project

capnm commented 10 months ago

In one sentence: Area doesn't monitor Bodies with a mask on the corresponding Area layer, it is a regression to v3.x in both 2D and 3D. EDIT: As mentioned below, it was intentionally broken aka fixed in https://github.com/godotengine/godot/pull/51801...

voidexp commented 10 months ago

Actually, this seems to be the fixed behavior in Godot 4, as by #51801. The 3.x behavior was a source of a lot of confusion, I've even found my own old comments on the subject as long as #15243, and there were plenty of similar issues (and actual bugs too).

The proposal (and the related fixes) which addresses this can be found here: https://github.com/godotengine/godot-proposals/issues/2775

Documentation definitely needs a clear paragraph with examples about how layers and masks are expected to interact, for sure.

AFAIK, this is not a bug tho.

fuzhouch commented 10 months ago

Thanks @voidexp for clarification! So it is indeed a quirk in Godot 3. Yes a documentation will be helpful. Though my workaround mentioned in bug report (seems we should not call it workaround anymore...) has its side-effect, it's not a showstopper.

A crazy thought quickly jumps in my mind: I'm thinking a comparison project btw 3 and 4 may be helpful to game developers who work on migration projects like me. I'm happy to contribute my comparison project if Godot team think it useful.

voidexp commented 10 months ago

There’s a doc about migration from Godot 3 to 4, and perhaps you can PR about things not mentioned there that you’ve found and reproduced.

voidexp commented 10 months ago

Also, the automatic project conversion tool can benefit from a PR too, if there’s a clear one-way fix for some of the scenarios in your demo project, although I’m not really familiar with its workings.

fuzhouch commented 10 months ago

Thanks Ivan @voidexp, could you guide me how to do PR on the documentation?

Actually, I opened around 5 issues in the past 2 weeks. 2 have been confirmed to be undocumented design change while some are really bugs. Unfortunately, I did read migration doc before migrating, but didn't find corresponding answer, that's why I chose to open issues.

voidexp commented 10 months ago

@fuzhouch - there's documentation about... how to contribute to documentation :) https://docs.godotengine.org/en/latest/contributing/documentation/contributing_to_the_documentation.html

Deozaan commented 10 months ago

I think the proper way to do what you want is to tell the dead enemy to add a collision exception with the player when it is dead, and then remove the exception if it gets reactivated by the flame. There's no need to mess around with collision layers and masks at runtime in this example.

https://docs.godotengine.org/en/4.0/classes/class_physicsbody2d.html#class-physicsbody2d-method-add-collision-exception-with

EDIT: I thought this worked for all CollisionObject2Ds, but I see now that function only applies to PhysicsBody2Ds, but not Area2Ds. So I don't think it will work for your purposes after all.

fuzhouch commented 10 months ago

Thanks @Deozaan for the suggestion! I didn't notice an API since 3.5. New things is learned.

Unfortunately this API may not be a good solution just as you pointed out. In my game, the dead enemy does not respond only to player, but also almost all enemies. Think about an idea: You see a Pumpkin Head is defeated by a ghost hunter. It should be harmless to everyone, but fire can bring it back to life. This is a typical scenario for collision masks, that I assign a mask to all fire-related Area2D, like fireballs, explosions or bonfires. These effects can be triggerred from different characters, but not a single Node.

And thank you @voidexp for pointing out the link. I may need some time to finish my current project this month. Then I will summrize all my findings and request updating the doc. Hopefully it can minimize the destraction I made to team. :)