Search code examples
godotgdscript

Overriding KinematicBody2D movement within Area2D?


I'm trying to create a windy area within which the player would be pushed continuously to the left <-
enter image description here

So far this is what I've come up with for the WindyArea:

extends Area2D

var bodies_within=[] # saves bodies within the area which have "direction" property

func _physics_process(delta): # the "wind" process
    for body in bodies_within:
        body.direction.x-=50 # constantly pushing x back by 50
        body.direction=body.move_and_slide(body.direction) 

func _body_entered(body):
    if("direction" in body): # if body has direction then append it to the array
        bodies_within.append(body)
        set_physics_process(true) # turn on the "wind"

func _body_exited(body):
    if(body in bodies_within):
        bodies_within.erase(body)
    
    if!(len(bodies_within)): # if no bodies are left within the area then turn off the "wind"
        set_physics_process(false)

func _ready():
    set_physics_process(false)
    
    self.connect("body_entered",self,"_body_entered")
    self.connect("body_exited",self,"_body_exited")

and for my Player body (just for reference, I'm trying not to change this script and having to create a new windy function in it):

extends KinematicBody2D

var direction:Vector2
var gravity_speed=4500
var jump_speed=-1500
var descend_speed=50
var horizontal_speed=600

func _physics_process(delta):
    direction.x=Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    direction.x*=horizontal_speed
    
    if(is_on_floor()):
        if(Input.is_action_just_pressed("ui_up")):
            direction.y=jump_speed
    
    ## Movement ##
    direction.y += gravity_speed * delta + descend_speed * Input.get_action_strength("ui_down")
    direction=move_and_slide(direction,Vector2.UP)

but it only works in pushing the player back when the player is not given any user input and the body is static, but when the player is given user input it does not seem to work, instead it starts moving faster (I'm guessing since move_and_slide is being called twice?)

So is there any fix or alternate solution which involves the same approach of not having to change the player script (much) and instead hands the movement alteration to the Area2D?


Solution

  • I'm going to offer an entirely different approach: gravity overrides.

    Most tutorials out there have you hard code the gravity in the player character. In fact, you are doing that. Well, As Dr. Evil would put it: How about "No"?


    There is a global gravity that you can get from project settings. You can get like this:

    var g_scale:float = ProjectSettings.get("physics/2d/default_gravity")
    var g:Vector2 = ProjectSettings.get("physics/2d/default_gravity_vector") * g_scale
    

    Which, perhaps you don't want. However, you might find useful to express the gravity you want as a multiple of this gravity.

    var g_scale:float = ProjectSettings.get("physics/2d/default_gravity")
    var g:Vector2 = ProjectSettings.get("physics/2d/default_gravity_vector") * g_scale
    
    gravity = g * g_factor
    

    Why? Because we are going to use the Area2D to modify the gravity, and then you can apply the same multiplier to the gravity you get.

    So, as you might be aware, the Area2D has "physics override" properties which allow you to modify the gravity.

    Which begs the question? How do you query the gravity under which your kinematic body is? Well, like this:

    var gravity = Physics2DServer.body_get_direct_state(get_rid()).total_gravity
    

    And, yes, you could apply a factor:

    gravity = Physics2DServer.body_get_direct_state(get_rid()).total_gravity * g_factor
    

    Now you use this gravity for your motion. And you make an Area2D that overrides the default gravity with an horizontal component. The player character falls according to the modified gravity, which also means it moves side ways. Ta-da! "wind".

    Be aware that this implies to drop the assumption that gravity is along the y axis. You could do it like this: direction += gravity * delta.


    Ok, ok, ok, let us say you don't want to do that. What you could do is expose either a velocity or acceleration property that the Area2D can set to the bodies that enter.

    For example:

    var area_effect_velocity:Vector2
    

    Then the Area2D does this:

    func _body_entered(body):
        if("area_effect_velocity" in body):
            bodies_within.append(body)
            body.area_effect_velocity = Vector2(-50.0, 0.0)
    
    func _body_exited(body):
        if(body in bodies_within):
            bodies_within.erase(body)
            body.area_effect_velocity = Vector2.ZERO
    

    Then you can add that in your motion. For example:

    direction += area_effect_velocity
    

    Addendum on overlapping Area2D:

    With the gravity override approach, you can set the space override to "Combine", and Godot solves that for you.

    With the other approach, you can handle it by offsets:

    var effect_velocity := Vector2(-50.0, 0.0)
    
    func _body_entered(body):
        if("area_effect_velocity" in body):
            bodies_within.append(body)
            body.area_effect_velocity += effect_velocity
    
    func _body_exited(body):
        if(body in bodies_within):
            bodies_within.erase(body)
            body.area_effect_velocity -= effect_velocity
    

    And that would avoid extra checking.