"FauxBody2D"
We are going to make a custom Node
which I'm calling FauxBody2D
. It will work a RigidBody2D
with a CollisionShape
and as an Area2D
with the same CollisionShape
. And to archive this, we will use Physics2DServer
.
Even though the first common ancestor class of RigidBody2D
and Area2D
is CollisionObject2D
, it is not convenient to extend CollisionObject2D
. So FauxBody2D
will be of type Node2D
.
So create a new script faux_body.gd
. However, it is not intended to be used directly in the scene tree (you can, but you won't be able to extend its code), instead to use it add a Node2D
with a new script and set it extends FauxBody2D
.
You would be able to the variables of FauxBody2D
and mess with it in undesirable ways. In fact, even though I'm declaring setters, your script would bypass them if you don't use self
. For example, don't set applied_force
, set self.applied_force
instead. By the way, some methods are left empty for you to override in your script (they are "virtual").
These are our firsts lines of code in faux_body.gd
:
class_name FauxBody2D
extends Node2D
I will avoid repeating code.
Mimic RigidBody2D
I'm skipping rough
, absorbent
. Also In this answer I only show monitoring and signals with area. See the followup answer.
We are going to create a body in _enter_tree
and free it in _exit_tree
:
var _body:RID
var _invalid_rid:RID
func _enter_tree() -> void:
_body = Physics2DServer.body_create()
func _exit_tree() -> void:
Physics2DServer.free_rid(_body)
_body = _invalid_rid
There is no expression to get a zeroed RID
. I will declare a _invalid_rid
and never set it, so it is always zeroed.
Also the body should be in the same space as the FauxBody2D
:
func _enter_tree() -> void:
# …
Physics2DServer.body_set_space(_body, get_world_2d().space)
Mimic CollisionShape2D
Next let us implement the logic for the CollisionShape2D
:
export var shape:Shape2D setget set_shape
export var disabled:bool setget set_disabled
export var one_way_collision:bool setget set_one_way_collision
export(float, 0.0, 128.0) var one_way_collision_margin:float setget set_one_way_collision_margin
var _shape:RID
func _enter_tree() -> void:
# …
_update_shape()
func _update_shape() -> void:
var new_shape = _invalid_rid if shape == null else shape.get_rid()
if new_shape == _shape:
return
if _shape.get_id() != 0:
Physics2DServer.body_remove_shape(_body, 0)
_shape = new_shape
if _shape.get_id() != 0:
Physics2DServer.body_add_shape(_body, _shape, Transform2D.IDENTITY, disabled)
Physics2DServer.body_set_shape_as_one_way_collision(_body, 0, one_way_collision, one_way_collision_margin)
func set_shape(new_value:Shape2D) -> void:
if shape == new_value:
return
shape = new_value
if _body.get_id() == 0:
return
_update_shape()
func set_disabled(new_value:bool) -> void:
if disabled == new_value:
return
disabled = new_value
if _body.get_id() == 0:
return
if _shape.get_id() != 0:
Physics2DServer.body_set_shape_disabled(_body, 0, disabled)
func set_one_way_collision(new_value:bool) -> void:
if one_way_collision == new_value:
return
one_way_collision = new_value
if _body.get_id() == 0:
return
if _shape.get_id() != 0:
Physics2DServer.body_set_shape_as_one_way_collision(_body, 0, one_way_collision, one_way_collision_margin)
func set_one_way_collision_margin(new_value:float) -> void:
if one_way_collision_margin == new_value:
return
one_way_collision_margin = new_value
if _body.get_id() == 0:
return
if _shape.get_id() != 0:
Physics2DServer.body_set_shape_as_one_way_collision(_body, 0, one_way_collision, one_way_collision_margin)
Here I'm using _invalid_rid
when the shape
is not valid. Notice that we are not responsible of freeing the shape RID
.
State
With this done the body will work as a RigidBody2D
but children of the FauxBody2D
are not children of the body. We will take advantage of integrate forces, and while we are at it set the state of the body.
signal sleeping_state_changed()
export var linear_velocity:Vector2 setget set_linear_velocity
export var angular_velocity:float setget set_angular_velocity
export var can_sleep:bool = true setget set_can_sleep
export var sleeping:bool setget set_sleeping
export var custom_integrator:bool setget set_custom_integrator
func _enter_tree() -> void:
# …
Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_TRANSFORM, global_transform)
Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_ANGULAR_VELOCITY, angular_velocity)
Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_CAN_SLEEP, can_sleep)
Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_LINEAR_VELOCITY, linear_velocity)
Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_SLEEPING, sleeping)
Physics2DServer.body_set_force_integration_callback(_body, self, "_body_moved", 0)
Physics2DServer.body_set_omit_force_integration(_body, custom_integrator)
func _body_moved(state:Physics2DDirectBodyState, _user_data) -> void:
_integrate_forces(state)
global_transform = state.transform
angular_velocity = state.angular_velocity
linear_velocity = state.linear_velocity
if sleeping != state.sleeping:
sleeping = state.sleeping
emit_signal("sleeping_state_changed")
# warning-ignore:unused_argument
func _integrate_forces(state:Physics2DDirectBodyState) -> void:
pass
func set_linear_velocity(new_value:Vector2) -> void:
if linear_velocity == new_value:
return
linear_velocity = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_LINEAR_VELOCITY, linear_velocity)
func set_angular_velocity(new_value:float) -> void:
if angular_velocity == new_value:
return
angular_velocity = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_ANGULAR_VELOCITY, angular_velocity)
func set_can_sleep(new_value:bool) -> void:
if can_sleep == new_value:
return
can_sleep = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_CAN_SLEEP, can_sleep)
func set_sleeping(new_value:bool) -> void:
if sleeping == new_value:
return
sleeping = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_SLEEPING, sleeping)
func set_custom_integrator(new_value:bool) -> void:
if custom_integrator == new_value:
return
custom_integrator = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_omit_force_integration(_body, custom_integrator)
The body will start at the global_transform
of our FauxBody2D
, and when the body moves we get a callback in _body_moved
where update the properties of the FauxBody2D
to match the state of the body, including the global_transform
of the FauxBody2D
. Now you can add children to the FauxBody2D
and they will move according to the body.
However, when the FauxBody2D
moves, it does not move the body. I will solve it with NOTIFICATION_TRANSFORM_CHANGED
:
func _enter_tree() -> void:
# …
set_notify_transform(true)
func _notification(what: int) -> void:
if what == NOTIFICATION_TRANSFORM_CHANGED:
if _body.get_id() != 0:
Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_TRANSFORM, global_transform)
By the way if _body.get_id() != 0:
should be the same as if _body:
but I prefer to be explicit.
Now when the FauxBody2D
moves (not when its transform is set) it will update the transform of the body.
Parameters
Next I will deal with body parameters:
export(float, EXP, 0.01, 65535.0) var mass:float = 1.0 setget set_mass
export(float, EXP, 0.0, 65535.0) var inertia:float = 1.0 setget set_inertia
export(float, 0.0, 1.0) var bounce:float = 0.0 setget set_bounce
export(float, 0.0, 1.0) var friction:float = 1.0 setget set_friction
export(float, -128.0, 128.0) var gravity_scale:float = 1.0 setget set_gravity_scale
export(float, -1.0, 100.0) var linear_damp:float = -1 setget set_linear_damp
export(float, -1.0, 100.0) var angular_damp:float = -1 setget set_angular_damp
func _enter_tree() -> void:
# …
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_ANGULAR_DAMP, angular_damp)
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_GRAVITY_SCALE, gravity_scale)
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_INERTIA, inertia)
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_LINEAR_DAMP, linear_damp)
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_MASS, mass)
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_BOUNCE, bounce)
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_FRICTION, friction)
# …
func set_mass(new_value:float) -> void:
if mass == new_value:
return
mass = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_MASS, mass)
func set_inertia(new_value:float) -> void:
if inertia == new_value:
return
inertia = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_INERTIA, inertia)
func set_bounce(new_value:float) -> void:
if bounce == new_value:
return
bounce = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_BOUNCE, bounce)
func set_friction(new_value:float) -> void:
if friction == new_value:
return
friction = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_FRICTION, friction)
func set_gravity_scale(new_value:float) -> void:
if gravity_scale == new_value:
return
gravity_scale = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_GRAVITY_SCALE, gravity_scale)
func set_linear_damp(new_value:float) -> void:
if linear_damp == new_value:
return
linear_damp = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_LINEAR_DAMP, linear_damp)
func set_angular_damp(new_value:float) -> void:
if angular_damp == new_value:
return
angular_damp = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_ANGULAR_DAMP, angular_damp)
I believe the pattern is clear.
By the way inertia
is not exposed in RigidBody2D
in Godot 3.x but it is in Godot 4.0, so went ahead and added it here.
Continuos integration
export(int, "Disabled", "Cast Ray", "Cast Shape") var continuous_cd
func _enter_tree() -> void:
# …
Physics2DServer.body_set_continuous_collision_detection_mode(_body, continuous_cd)
# …
func set_continuous_cd(new_value:int) -> void:
if continuous_cd == new_value:
return
continuous_cd = new_value
if _body.get_id() == 0:
return
Physics2DServer.body_set_continuous_collision_detection_mode(_body, continuous_cd)
Force and torque
We will accumulate these in applied_force
and torque
. I will take another page form Godot 4.0 and have a center_of_mass
. In consequence I will not use body_add_force
, instead I will do the equivalent body_add_center_force
and body_add_torque
calls, so I can compute the torque
with the custom center_of_mass
.
Furthermore, Godot has a discrepancy between 2D and 3D that in 3D forces are reset every physics frame, but not in 2D. So I want it to be configurable. For that I'm adding a auto_reset_forces
property.
export var applied_force:Vector2 setget set_applied_force
export var applied_torque:float setget set_applied_torque
export var center_of_mass:Vector2
export var auto_reset_forces:bool
func _enter_tree() -> void:
# …
Physics2DServer.body_add_central_force(_body, applied_force)
Physics2DServer.body_add_torque(_body, applied_torque)
func _body_moved(state:Physics2DDirectBodyState, _user_data) -> void:
# …
if auto_reset_forces:
Physics2DServer.body_add_central_force(_body, -applied_force)
Physics2DServer.body_add_torque(_body, -applied_torque)
applied_force = Vector2.ZERO
applied_torque = 0
func add_central_force(force:Vector2) -> void:
applied_force += force
if _body.get_id() != 0:
Physics2DServer.body_add_central_force(_body, force)
func add_force(force:Vector2, offset:Vector2) -> void:
var torque := (offset - center_of_mass).cross(force)
applied_force += force
applied_torque += torque
if _body.get_id() != 0:
Physics2DServer.body_add_central_force(_body, force)
Physics2DServer.body_add_torque(_body, torque)
func add_torque(torque:float) -> void:
applied_torque += torque
if _body.get_id() != 0:
Physics2DServer.body_add_torque(_body, torque)
func apply_central_impulse(impulse:Vector2) -> void:
if _body.get_id() != 0:
Physics2DServer.body_apply_central_impulse(_body, impulse)
func apply_impulse(offset:Vector2, impulse:Vector2) -> void:
if _body.get_id() != 0:
Physics2DServer.body_apply_impulse(_body, offset, impulse)
func apply_torque_impulse(torque:float) -> void:
if _body.get_id() != 0:
Physics2DServer.body_apply_torque_impulse(_body, torque)
func set_applied_force(new_value:Vector2) -> void:
if applied_force == new_value:
return
if _body.get_id() != 0:
var difference := new_value - applied_force
Physics2DServer.body_add_central_force(_body, difference)
applied_force = new_value
func set_applied_torque(new_value:float) -> void:
if applied_torque == new_value:
return
if _body.get_id() != 0:
var difference := new_value - applied_torque
Physics2DServer.body_add_torque(_body, difference)
applied_torque = new_value
By the way, I haven't really experimented with applying forces and torque to physics bodies before adding them to the scene tree (I don't know why I would do that). Yet, it makes sense to me that the applied forces and torque would be stored and applied when the body enters the scene tree. And by the way, I'm not erasing them when the body exits the scene tree.
Collision exceptions
And we run into a function that is not exposed to scripting: body_get_collision_exceptions
. So we will have to keep inventory of the collision exceptions. This is fine, it means I can get away with storing them before creating the body.
var collision_exceptions:Array
func add_collision_exception_with(body:Node) -> void:
var collision_object := body as PhysicsBody2D
if not is_instance_valid(collision_object):
push_error( "Collision exception only works between two objects of PhysicsBody type.")
return
var rid = collision_object.get_rid()
if rid.get_id() == 0:
return
if collision_exceptions.has(collision_object):
return
collision_exceptions.append(collision_object)
if _body.get_id() != 0:
Physics2DServer.body_add_collision_exception(_body, rid)
func get_collision_exceptions() -> Array:
return collision_exceptions
func remove_collision_exception_with(body:Node) -> void:
var collision_object := body as PhysicsBody2D
if not is_instance_valid(collision_object):
push_error( "Collision exception only works between two objects of PhysicsBody type.")
return
var rid = collision_object.get_rid()
if rid.get_id() == 0:
return
if not collision_exceptions.has(collision_object):
return
collision_exceptions.erase(collision_object)
if _body.get_id() != 0:
Physics2DServer.body_remove_collision_exception(_body, rid)
Test motion
This one is very simple:
func test_motion(motion:Vector2, infinite_inertia:bool = true, margin:float = 0.08, result:Physics2DTestMotionResult = null) -> bool:
if _body.get_id() == 0:
push_error("body is not inside the scene tree")
return false
return Physics2DServer.body_test_motion(_body, global_transform, motion, infinite_inertia, margin, result)
By the way, in case you want to pass the exclude
parameter of body_test_motion
, know that it wants RID
s. And just in case, I'll also mention that get_collision_exceptions
is documented to return Node
s, and that is how I implemented it here.
Axis velocity
While I'm tempted to implement it like this:
func set_axis_velocity(axis_velocity:Vector2) -> void:
Physics2DServer.body_set_axis_velocity(_body, axis_velocity)
It is not really convenient. The reason being that I want to continue with the idea of storing the properties and apply them when the body enters the scene tree.
For an alternative way to implement this, we should understand what it does: it changes the linear_velocity
but only on the direction of axis_velocity
, any perpendicular velocity would not be affected. In other words, we decompose linear_velocity
in velocity along axis_velocity
and velocity perpendicular to axis_velocity
, and then we compute a new linear_velocity
from the axis_velocity
plus the component of the old linear_velocity
that is perpendicular to axis_velocity
.
So, like this:
func set_axis_velocity(axis_velocity:Vector2) -> void:
self.linear_velocity = axis_velocity + linear_velocity.slide(axis_velocity.normalized())
By the way, the reason why the official documentation says that axis_velocity
is useful for jumps is because it allows you to set the vertical velocity without affecting the horizontal velocity.
Mimic Area2D
I will not implement space overrides et.al. Hopefully you have by now a good idea of how to interact with Physics2DServer
, suffice to say you would want to use the area_*
methods instead of the body_*
methods. So you can set the area parameters with area_set_param
and the space override mode with area_set_space_override_mode
.
Next, let us create an area:
var _area:RID
func _enter_tree() -> void:
_area = Physics2DServer.area_create()
Physics2DServer.area_set_space(_area, get_world_2d().space)
Physics2DServer.area_set_transform(_area, global_transform)
# …
func _exit_tree() -> void:
Physics2DServer.free_rid(_area)
_area = _invalid_rid
# …
Note: I am also giving position to the area with area_set_transform
.
And let us attach the shape to the area:
func _update_shape() -> void:
# …
if _shape.get_id() != 0:
Physics2DServer.area_add_shape(_area, _shape, Transform2D.IDENTITY, disabled)
# …
Move the area
We should also move the area when the body moves:
func _notification(what: int) -> void:
if what == NOTIFICATION_TRANSFORM_CHANGED:
# …
if _area.get_id() != 0:
Physics2DServer.area_set_transform(_area, global_transform)
Modes
I want copy Godot 4.0 design and use freeze
and freeze_mode
instead of using mode
. Then converting our Node2D
will eventually be an extra freeze_mode
. It could also be an extra mode
if I do it more like Godot 3.x.
export var lock_rotation:bool setget set_lock_rotation
export var freeze:bool setget set_freeze
export(int, "Static", "Kinematic", "Area") var freeze_mode:int setget set_freeze_mode
func _enter_tree() -> void:
# …
_update_body_mode()
func _update_body_mode() -> void:
if freeze:
if freeze_mode == 1:
Physics2DServer.body_set_mode(_body, Physics2DServer.BODY_MODE_KINEMATIC)
else:
Physics2DServer.body_set_mode(_body, Physics2DServer.BODY_MODE_STATIC)
else:
if lock_rotation:
Physics2DServer.body_set_mode(_body, Physics2DServer.BODY_MODE_CHARACTER)
else:
Physics2DServer.body_set_mode(_body, Physics2DServer.BODY_MODE_RIGID)
func set_lock_rotation(new_value:bool) -> void:
if lock_rotation == new_value:
return
lock_rotation = new_value
if _body.get_id() == 0:
return
_update_body_mode()
func set_freeze(new_value:bool) -> void:
if freeze == new_value:
return
freeze = new_value
if _body.get_id() == 0:
return
_update_body_mode()
func set_freeze_mode(new_value:int) -> void:
if freeze_mode == new_value:
return
freeze_mode = new_value
if _body.get_id() == 0:
return
_update_body_mode()
Since I implemented _update_body_mode
checking if freeze_mode
is Kinematic
, "Area"
will behave as "Static"
, which is what we want. Well, almost, we will get to that.
Input Pickable
Sadly body_set_pickable
is not exposed for scripting. So we will have to recreate this functionality.
signal input_event(viewport, event, shape_idx)
signal mouse_entered()
signal mouse_exited()
export var input_pickable:bool setget set_input_pickable
var _mouse_is_inside:bool
func _enter_tree() -> void:
# …
_update_pickable()
# …
func _notification(what: int) -> void:
# …
if what == NOTIFICATION_VISIBILITY_CHANGED:
_update_pickable()
func _update_pickable() -> void:
set_process_input(input_pickable and _body.get_id() != 0 and is_visible_in_tree())
func _input(event: InputEvent) -> void:
if (
not (event is InputEventScreenDrag)
and not (event is InputEventScreenTouch)
and not (event is InputEventMouse)
):
return
var viewport := get_viewport()
var position:Vector2 = viewport.get_canvas_transform().affine_inverse().xform(event.position)
var objects := get_world_2d().direct_space_state.intersect_point(position, 32, [], 0x7FFFFFFF, false, true)
var is_inside := false
for object in objects:
if object.rid == _area:
is_inside = true
break
if is_inside:
_input_event(viewport, event, 0)
emit_signal("input_event", viewport, event, 0)
if event is InputEventMouse and _mouse_is_inside != is_inside:
_mouse_is_inside = is_inside
if _mouse_is_inside:
emit_signal("mouse_entered")
else:
emit_signal("mouse_exited")
# warning-ignore:unused_argument
# warning-ignore:unused_argument
# warning-ignore:unused_argument
func _input_event(viewport:Object, event:InputEvent, shape_idx:int) -> void:
pass
func set_input_pickable(new_value:bool) -> void:
if input_pickable == new_value:
return
input_pickable = new_value
_update_pickable()
Body entered and exited
There seems to be a bug that area_set_monitorable
is needed to monitor too. So we cannot make an area that monitors but is not monitorable.
signal body_entered(body)
signal body_exited(body)
signal body_shape_entered(body_rid, body, body_shape_index, local_shape_index)
signal body_shape_exited(body_rid, body, body_shape_index, local_shape_index)
signal area_entered(area)
signal area_exited(area)
signal area_shape_entered(area_rid, area, area_shape_index, local_shape_index)
signal area_shape_exited(area_rid, area, area_shape_index, local_shape_index)
export var monitorable:bool setget set_monitorable
export var monitoring:bool setget set_monitoring
var overlapping_body_instances:Dictionary
var overlapping_area_instances:Dictionary
var overlapping_bodies:Dictionary
var overlapping_areas:Dictionary
func _enter_tree() -> void:
# …
_update_monitoring()
# …
func _update_monitoring() -> void:
Physics2DServer.area_set_monitorable(_area, monitorable or monitoring)
if monitoring:
Physics2DServer.area_set_monitor_callback(_area, self, "_body_monitor")
Physics2DServer.area_set_area_monitor_callback(_area, self, "_area_monitor")
else:
Physics2DServer.area_set_monitor_callback(_area, null, "")
Physics2DServer.area_set_area_monitor_callback(_area, null, "")
func _body_monitor(status:int, body:RID, instance_id:int, body_shape_index:int, local_shape_index:int) -> void:
if _body == body:
return
var instance := instance_from_id(instance_id)
if status == 0:
# entered
if not overlapping_bodies.has(body):
overlapping_bodies[body] = 0
overlapping_body_instances[instance] = instance
emit_signal("body_entered", instance)
overlapping_bodies[body] += 1
emit_signal("body_shape_entered", body, instance, body_shape_index, local_shape_index)
else:
# exited
emit_signal("body_shape_exited", body, instance, body_shape_index, local_shape_index)
overlapping_bodies[body] -= 1
if overlapping_bodies[body] == 0:
overlapping_bodies.erase(body)
overlapping_body_instances.erase(instance)
emit_signal("body_exited", instance)
func _area_monitor(status:int, area:RID, instance_id:int, area_shape_index:int, local_shape_index:int) -> void:
var instance := instance_from_id(instance_id)
if status == 0:
# entered
if not overlapping_areas.has(area):
overlapping_areas[area] = 0
overlapping_area_instances[instance] = instance
emit_signal("area_entered", instance)
overlapping_areas[area] += 1
emit_signal("area_shape_entered", area, instance, area_shape_index, local_shape_index)
else:
# exited
emit_signal("area_shape_exited", area, instance, area_shape_index, local_shape_index)
overlapping_areas[area] -= 1
if overlapping_areas[area] == 0:
overlapping_areas.erase(area)
overlapping_area_instances.erase(instance)
emit_signal("area_exited", instance)
func get_overlapping_bodies() -> Array:
if not monitoring:
push_error("monitoring is false")
return []
return overlapping_body_instances.keys()
func get_overlapping_areas() -> Array:
if not monitoring:
push_error("monitoring is false")
return []
return overlapping_area_instances.keys()
func overlaps_body(body:Node) -> bool:
if not monitoring:
return false
return overlapping_body_instances.has(body)
func overlaps_area(area:Node) -> bool:
if not monitoring:
return false
return overlapping_area_instances.has(area)
func set_monitoring(new_value:bool) -> void:
if monitoring == new_value:
return
monitoring = new_value
if _area.get_id() == 0:
return
_update_monitoring()
func set_monitorable(new_value:bool) -> void:
if monitorable == new_value:
return
monitorable = new_value
if _area.get_id() == 0:
return
_update_monitoring()
Here I'm using area_set_monitor_callback
and area_set_area_monitor_callback
. The documentation claims that area_set_monitor_callback
works for both areas and bodies. However that is nor correct. area_set_monitor_callback
is only for bodies, and the undocumented area_set_area_monitor_callback
is for areas.
I need to keep track of each shape that enters and exists. Which is why I'm using dictionaries for overlapping_areas
and overlapping_bodies
. The keys will be the RID
s, and the values will be the number of shape overlaps.
We are almost done.
Collision layer and mask
I want both area and body to share collision layer and mask. Except in "Area"
mode, where I'll set the collision layer and mask of the body to 0 so it does not collide with anything.
export(int, LAYERS_2D_PHYSICS) var collision_layer:int = 1 setget set_collision_layer
export(int, LAYERS_2D_PHYSICS) var collision_mask:int = 1 setget set_collision_mask
func _enter_tree() -> void:
# …
_update_collision_layer_and_mask()
# …
func _update_collision_layer_and_mask() -> void:
Physics2DServer.area_set_collision_layer(_area, collision_layer)
Physics2DServer.body_set_collision_layer(_body, collision_layer if not freeze or freeze_mode != 2 else 0)
Physics2DServer.area_set_collision_mask(_area, collision_mask)
Physics2DServer.body_set_collision_mask(_body, collision_mask if not freeze or freeze_mode != 2 else 0)
func set_collision_layer(new_value:int) -> void:
if collision_layer == new_value:
return
collision_layer = new_value
if _body.get_id() == 0:
return
_update_collision_layer_and_mask()
func set_collision_mask(new_value:int) -> void:
if collision_mask == new_value:
return
collision_mask = new_value
if _body.get_id() == 0:
return
_update_collision_layer_and_mask()
And while we are at it let us implement get_collision_layer_bit
, get_collision_mask_bit
, set_collision_layer_bit
, and set_collision_mask_bit
:
func get_collision_layer_bit(bit:int) -> bool:
if bit < 0 or bit > 31:
push_error("Collision layer bit must be between 0 and 31 inclusive.")
return false
return collision_layer & (1 << bit) != 0
func get_collision_mask_bit(bit:int) -> bool:
if bit < 0 or bit > 31:
push_error("Collision mask bit must be between 0 and 31 inclusive.")
return false
return collision_mask & (1 << bit) != 0
func set_collision_layer_bit(bit:int, value:bool) -> void:
if bit < 0 or bit > 31:
push_error("Collision layer bit must be between 0 and 31 inclusive.")
return
if value:
self.collision_layer = collision_layer | 1 << bit
else:
self.collision_layer = collision_layer & ~(1 << bit)
func set_collision_mask_bit(bit:int, value:bool) -> void:
if bit < 0 or bit > 31:
push_error("Collision mask bit must be between 0 and 31 inclusive.")
return
if value:
self.collision_mask = collision_mask | 1 << bit
else:
self.collision_mask = collision_mask & ~(1 << bit)
And add a call to _update_collision_layer_and_mask()
in _update_body_mode
:
func _update_body_mode() -> void:
_update_collision_layer_and_mask()
# …
And we are done. I think.
Node
s altogether and talking toPhysics2DServer
to be better. I might write an answer later. – InvidiousPhysics2DServer
? The documentations don't seem to have any tutorial that can help with beginners – LupienNode
s are a convenient way to do scenes, but they are actually using "servers" behind the scenes.Physics2DServer
is the one responsible for physics in 2D,VisualServer
is responsible for graphics, and such. So, for example, If I need to create andArea2D
I can useNode
s or I can make a bunch ofPhysics2DServer
calls that accomplish the same thing, but without creatingNode
s, which means less overhead. Thus if your goal is performance, usingPhysics2DServer
should be better. – InvidiousPhysics2DServer
handles all the physics without having to create a node? That sounds a little counter intuitive, do you mean like I can create aPosition2D
node and somehow applyPhysics2DServer
to make it behave like aRigidBody2D
and/orArea2D
? – LupienNode
s to have physics, you don't exactly applyPhysics2DServer
to aNode
, and yes you could make aPosition2D
behave like aRigidBody2D
or anArea2D
.Physics2DServer
is a level or abstraction belowNode
s. I'm currently writing the code for the answer, but it is long code (I don't know what exactly you need ofRigidBody2D
, so I'm making it all - it is fine, it might help as tutorial for other people, the documentation is sacarse anyway), it will take a while. – InvidiousNode
equals a physics thing. ANode
could be many physics things. Imagine aBulletHell
customNode
that has anadd_bullet
method, and and emits a signal when any of the bullets collide. But does not useNode
s per bullet, instead it talks toPhysics2DServer
andVisualServer
. So adding more bullets does not mean allocating moreNode
s, which means less overhead, which means more performance, which means the game can have more bullets in play. – InvidiousRigidBody2D
i.e. like in this zombie dismemberment game you can see the limbs becoming rigid bodies after blowing up – LupienArea2D
when the inner nodeCollisionPolygon2D
's collision is disabled & behaves like aRigidBody2D
when collision is enabled – LupienBody is limited to 30000 characters; you entered 37369.
Ha! – InvidiousRigidBody2D
can detect collisions. And anArea2D
would report a body entered as soon as the guillotine touches it too. Edit: Well, that or my answer is not what you need. – Invidious