godotengine / godot

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

A variable behavior for KinematicBody move #7130

Closed eon-s closed 6 years ago

eon-s commented 7 years ago

Feat. request:

Since most new features will be added on 3.0 and that may include move_and_slide, I was thinking on the possibility to have more types of move, mostly for prototyping (fine tuning will always fall on pure move).

These are: -A move that push: something basic to push other colliders, common use on puzzle and some platformers. -A relative move: use transform to apply the movement, useful for FPS, third person (3D) and topdown (2D) games.

Then I also thought that combinations of slide, push and local moves could be needed and got this final idea:

Have a single move and some flags on the KinematicBody that affects the way move behave.

I have made a simple project with a basic controller and these options exposed on the inspector to show the idea:

kinemoves.zip

RebelliousX commented 7 years ago

The most important thing to add is a feature to allow KinematicBody2D to detect collisions using move function, without moving the kinematicbody to a non colliding position after having a collision. Seriously, pushing a box in a grid like game with fixed grid movements like Sokoban (pushing boxes in exact pixel amounts) , can't be done using kinematicbody or any physics body in Godot. I for one, had to write my own collision detection and use the direct space state just to get results (or I did an AABB collision checking before that, but the code was over complicated) There are several closed issues about two colliding kinematic bodies affecting each other (despite the fact that KinematicBody2D is not affected by forces, it really is affected by other moving objects).

Also, test_move() should report colliders without move()ing first.

eon-s commented 7 years ago

@RebelliousX well, with a move+push, the collider should be pushed the same too, then to fix in a grid you will need to store the collider and "stepify" the positions when main motion ends (easy to try with that sample project).

For test, maybe an extra parameter to test_move to allow set the internal colliding variable like move does.

RebelliousX commented 7 years ago

@eon-s well, the thing is, move() returns the difference of movement as a vector, which could contain a fraction of pixels (float numbers). If you try to use vector snapped() function, it could put the object in a colliding position again. Leading to a never ending shaking (1 pixel back and forth) movement of the object. Since the move function tries to free the object, while the code insists to put it at that location.

Using set_pos() and testing for collisions using direct space state solves all issues. But you you have to write a custom collision detection functions. I have no problems that way.

bojidar-bg commented 7 years ago

About move_and_push -- isn't it too much game-specific? Here are some considerations:

  1. How would one specify how much "mass" can be pushed? E.g. in sokoban you can push just one thing at once (if I remember right), while in other games you can push rows of stuff around.
  2. How would you specify if it should move more slowly when pushing or whatever (leads to cool effects IMO)
  3. How would you handle cases where you push a rigidbody with a kinematicbody behind it? (refs. point 1).
  4. Maybe others?
eon-s commented 7 years ago

@bojidar-bg move_and_push, like _and_slide, is too game-specific, that is why I like the general behavior options more than just a method.

About 1, kinematic bodies are something of infinite mass, they should push anything with the limit of their movement. On 2, that should be managed by the programmer when is_colliding, movement can be modified according to the collider data. 3, the rigid body should not be able to be crushed, if can't move, the "pusher" can't push. And I guess there are lot of more specific situations...

RebelliousX commented 7 years ago
  1. Kinematic bodies by their definition in Godot should not interact with physics. Current behavior of kinematic bodies is that if 2 kinematic bodies collide with each other (with enough velocity), one can move the other from its place, that shouldn't happen.

For Sokoban, It is the player object that is responsible for moving crates, once the player is about to collide with a crate, he should test if the collided object is push-able or not (there is another crate or wall blocking its path). If it is blocked, the player stops before colliding with the crate. That is how I do it for my current never ending project which is similar to sokoban but infinitely better :) But again, for the weird behavior of kinematic bodies, I opted not to use them and check for collisions myself.

For collisions in puzzle games, Colliding objects should stop before colliding not after, at least from the perspective of puzzle games like Sokoban. And that can be achieved by testing for a collision before moving. Which again test_move() doesn't return which objs it will be colliding with if the motion continues. For that I use raycasts, point/line intersections from direct space state.

  1. As @eon-s said.

  2. Refer to answer in point 1

  3. Maybe others?

Like trying to push two stacked object. Say the player can push 1 and only 1 object at a time. If the player is positioned between 2 aligned objects, he shouldn't be able to push nor move (he stops).

What about a friction-less collision? Say if the player pushes the bottom object of 2 stacked objs. But the top object remains stationary until there is no object under it, so it falls due to gravity. :)


I know all of the above are game specific stuff. But what would be great is if there is something general that makes creation of such games easy. That is, by stopping the movement just before colliding, not after. Since not all games require physics for movement. Heck, The first test game ever in Godot was 2D shoot em up, all objs are kinematic, and bullets stop the falling spaceships and meteors and render them suspended as long as I was shooting (some required more than one hit to be destroyed). Despite that being cool in a way (if it is about physics), but it is a disaster.

Anyways, for me I have found a solution. But it is very limited since all moving objs must have rectangular collision shapes.

blurymind commented 7 years ago

I too would like to see this feature Implemented! I wrote a function that does it, however it is a bit messy EDIT: I fixed some of its issues

extends KinematicBody2D
#extends Node2D

var direction = Vector2(0,0)
var startPos = Vector2(0,0)
var moving = false
const SPEED = 3

var world
func _ready():
    world = get_world_2d().get_direct_space_state()
    set_fixed_process(true)
    startPos = get_pos()

func _fixed_process(delta):
#   print(get_pos(),"start--",startPos)
    # check for colisions - perhaps move this to be checked only once per grid step
    var resultUp = world.intersect_point(get_pos()+Vector2(0,-GRID))
    var resultDown = world.intersect_point(get_pos()+Vector2(0,GRID))
    var resultLeft = world.intersect_point(get_pos()+Vector2(-GRID,0))
    var resultRight = world.intersect_point(get_pos()+Vector2(GRID,0))

    # check direction and if colides
    if Input.is_action_pressed("ui_up") and resultUp.empty():
        direction = Vector2(0,-1)
    elif Input.is_action_pressed("ui_down") and resultDown.empty():
        direction = Vector2(0,1)
    elif Input.is_action_pressed("ui_left") and resultLeft.empty():
        direction = Vector2(-1,0)
    elif Input.is_action_pressed("ui_right") and resultRight.empty():
        direction = Vector2(1,0)
    else:
        direction = Vector2(0,0)
    gridMovement(delta,SPEED,direction)

########## GRID MOVEMENT FUNCTION ##############
const GRID = 64
var curDir = null
var curMoveTarget = null
var movProgress = 0
var gridMoveKineticState = null
func gridMovement(delta,speed,direction):
    if moving:
        if movProgress < 1 or get_pos() != curMoveTarget:
            movProgress += speed * delta
            print ("delta:",delta," target:",curMoveTarget," progress:",movProgress)
            ## movProgress-0.15 would go back before going forward (interesting anticipation mechanic can be achieved)
            set_pos(startPos.linear_interpolate(curMoveTarget,movProgress))
        else:
            print("---reached target:",curMoveTarget," movProgress:",movProgress)
            moving = false

    if !moving:  #<----- runs only once when the player reaches next cell
        movProgress = 0
        if direction != Vector2(0,0): #the body must be moving in a direction to start the next step
            startPos = get_pos()
            curDir = direction
            curMoveTarget = startPos +Vector2(GRID * curDir.x,GRID*curDir.y)
            moving = true

Limitations: Speed can not be bigger than the grid size (64 in the above example)- otherwise collisions stop working

Ideally grid movement should be built into the kinematicbody2d node class imo Just a tick box to enable it and a Vector2 value to set the grid size

eon-s commented 7 years ago

@blurymind grid movements are complicated and I don't think there will be a generic solution (depends a lot on the design), also could depend on input on each frame making it harder to control.

I have tried lot of approaches, now I think of it as a process of idle->start motion->continue motion->stop, where "continue motion" needs to check inputs all the time and decide if stepify position and stop or continue until next cell.

For your code, in the Q&A you will get more answers than here.

blurymind commented 7 years ago

I could polish my function and simply put it out as a plugin I guess :) I know it's not perfect, some stuff could be improved

RebelliousX commented 7 years ago

I have a pretty elegant solution, no need for collision shapes, physics bodies or anything. Just sprites and it is very cheap to check for collisions and yet powerful and scalable (objects can have different sizes (width and height) as long as they are multiple of GRID size), you decide which objects you can collide with according to their type.

All of that in around 140 lines of code, I might upload that as plugin in asset store. My work is based on GDquest video for Construct 2 and 08bitWarrior video for Game Maker Studio.

GDquest: https://www.youtube.com/watch?v=K6zAECdwDP8 08bitWarrior: https://www.youtube.com/watch?v=7YsrHrCBd94 and https://www.youtube.com/watch?v=kxXu6iQY0yw&t=394s

I m really amazed at the results I got. For my needs, it resembles exactly the old game I am trying to remake. :)

blurymind commented 7 years ago

My method function works on any node that has set-pos and is also very clean. For colision it simply checks if an object exists, but i do like to use it with the tilemap node. Can you please share your code, i want to try it out. The function i shared has some problems still

On 12 Apr 2017 08:06, "Thaer Razeq" notifications@github.com wrote:

I have a pretty elegant solution, no need for collision shapes, physics bodies or anything. Just sprites and it is very cheap to check for collisions and yet powerful and scalable (objects can have different sizes as long as they are multiple of GRID size), you decide which objects you can collide with according to their type.

All of that in around 140 lines of code, I might upload that as plugin in asset store. My work is based on GDquest video for Construct 2 and 08bitWarrior video for Game Maker Studio.

GDquest: https://www.youtube.com/watch?v=K6zAECdwDP8 08bitWarrior: https://www.youtube.com/watch?v=7YsrHrCBd94 and https://www.youtube.com/watch?v=kxXu6iQY0yw&t=394s

I m really amazed at the results I got. For my needs, it resembles exactly the old game I am trying to remake. :)

β€” You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/godotengine/godot/issues/7130#issuecomment-293492573, or mute the thread https://github.com/notifications/unsubscribe-auth/AGMbVeLlR9AHG743OENG0YoC8j8IdNXKks5rvHgDgaJpZM4K0CS3 .

RebelliousX commented 7 years ago

@blurymind Gladly,

Note 1: Any cell in grid can contain more than one object. :)

Note 2: In constants file, I mentioned game object types which are constant integers or enums if you are using Godot v3. You replace those with the objects of your game.

Note 3: The most important thing, You have to provide the following for each node that should have grid movement and should have collisions: 1: In the _ready() of any node (even static ones like ground that will interact with collisions) we do:

Grid_Translator = Utilities.Grid_Mover.new(self)
Collision_Detector = Utilities.Collision_Detector.new(self, Utilities.COMMON_BLOCKING_TYPES)

2: Also provide 2 functions: for example:

func get_object_type():
       return Constants.SOLID
       pass

and another function:

func get_object_size():
      return Vector2(width, height) # width and height are multiples of Constants.GRID
      pass

These are crucial to get collision results such as objects and their types, whether you want to block movement or not is your choice depending on the collision results, for example, in my case, for my hero, Laser OFF barrier should not block the hero from passing but should block pushable objects like crates or kill enemies..etc! This kind of behavior is different for each node, but easy to check using utility classes that all moving objects instantiate from and all share the same grid Cells[][] data.

Tabs for code here are screwed, but in editor are fine.

Constants.gd file:

# MIT Licensed, COPYRIGHT (C) 2017 Thaer Razeq. If you use this in your code, leave this
# Copyright notice, you can do whatever you want with this without further notice. No warranties
# whatsoever regarding this code.
extends Node

const NUM_COLUMNS = 32  # 2048 room width / 64 grid size
const NUM_ROWS = 24     # 1536 room height / 64 grid size
const GRID = 64         # 64 pixels grid size

# general directions
const CENTER            = 0
const UP                = -1
const RIGHT             = 1
const DOWN              = 1
const LEFT              = -1
# commonly used vectors
const vGRID             = Vector2(64, 64)   # replace 64 with GRID when fixed by Godot
const vCENTER           = Vector2(0, 0)
const vUP               = Vector2(0, -1)
const vRIGHT            = Vector2(1, 0)
const vDOWN             = Vector2(0, 1)
const vLEFT             = Vector2(-1, 0)
const vUP_RIGHT         = Vector2(1, -1)
const vDOWN_RIGHT       = Vector2(1, 1)
const vDOWN_LEFT        = Vector2(-1, 1)
const vUP_LEFT          = Vector2(-1, -1)
# game objects' types on collision grid "Cells[][] matrix in Utilities.gd"
const RAPHAEL           = 1
const SOLID             = 2
const PLATFORM          = 3
const LASER_ON          = 4
const LASER_OFF         = 5
const PUSHABLE          = 6
const CONVEYOR          = 7
const ROPE              = 8
const LIFT              = 9
const ELEVATOR          = 10
const ENEMY             = 11
const TREASURE          = 12
const KEY               = 13
const DOOR              = 14
const ELIXIR            = 15
const OXYGEN            = 16
const INVINCIBILITY     = 17

Utilities.gd file:

# MIT Licensed, COPYRIGHT (C) 2017 Thaer Razeq. If you use this in your code, leave this
# Copyright notice, you can do whatever you want with this without further notice. No warranties
# whatsoever regarding this code.
extends Node

# Used for collision detection
var Cells = []

class GameObject:
    var obj = null
    var type = null
    var size = Vector2()

var COMMON_BLOCKING_TYPES = [
            Constants.SOLID, Constants.PUSHABLE, Constants.LIFT, Constants.ELEVATOR,
            Constants.LASER_ON, Constants.CONVEYOR, Constants.PLATFORM
            ]

# --------------------------------------------------
func _ready():
    # initialize Cells matrix with empty GameObject array
    for c in range(Constants.NUM_COLUMNS):
        Cells.append([])
        for r in range(Constants.NUM_ROWS):
            Cells[c].append([])
    pass

# -----------------------------------------------------
# ---------------- Grid Movement Class ----------------
# -----------------------------------------------------

class Grid_Mover:
    var obj         = null              # object we want moving between grid cells
    var is_moving   = false             # are we currently moving between cells?
    var curDir      = Vector2()         # current direction we are moving
    var prevDir     = Vector2()         # previous direction
    var current_speed = 0               # current speed while moving between cells
    var obj_grid_location = Vector2()   # location of object in grid for collision detection
    var obj_size    = Vector2()         # object size (width and height) divided by GRID
    var game_object_type = null         # type of object or node, e.g, SOLID, ENEMY...etc!
    var game_obj = null                 # structure to hold object information: object, type, size
    var distance_left = 0               # it starts with GRID distance, it decreases until it reaches 0.
    # --------------------------------------------------
    func _init(var objectToMove):
        obj = objectToMove
        obj_size = obj.get_object_size() / Constants.GRID
        game_object_type = obj.get_object_type()
        obj_grid_location = obj.get_pos().snapped(Constants.vGRID) / Constants.GRID
        add_object_to_cells()
        pass
    # --------------------------------------------------
    func move(var direction):
        # Grid Movement
        if (not is_moving):
            if (direction != Constants.vCENTER):
                is_moving = true
                curDir = direction
                distance_left = Constants.GRID
                current_speed = Game_Globals.SPEED * Game_Globals.global_speed
                remove_object_from_cells()
                obj_grid_location += curDir
                add_object_to_cells()

        if (is_moving):
            var pos = obj.get_pos()
            pos = pos + (curDir * current_speed)
            obj.set_pos(pos)
            distance_left -= current_speed
            if (distance_left <= 0):
                is_moving = false
                prevDir = curDir
                curDir = Constants.vCENTER
                current_speed = 0
        pass
    # --------------------------------------------------
    func get_prev_dir():
        return prevDir
        pass

    # --------------------------------------------------
    func get_cur_dir():
        return curDir
        pass
    # --------------------------------------------------
    func add_object_to_cells():
        var x_from = obj_grid_location.x
        var x_to = obj_grid_location.x + obj_size.width
        var y_from = obj_grid_location.y
        var y_to = obj_grid_location.y + obj_size.height
        # add obj to grid 
        for c in range(x_from, x_to):
            for r in range(y_from, y_to):
                if (c < Constants.NUM_COLUMNS and c >= 0 and r < Constants.NUM_ROWS and r >= 0):
                    var game_obj = Utilities.GameObject.new()
                    game_obj.obj = obj
                    game_obj.type = game_object_type
                    game_obj.size = obj_size
                    Utilities.Cells[c][r].push_back(game_obj)
        pass
    # --------------------------------------------------
    func remove_object_from_cells():
        var x_from = obj_grid_location.x
        var x_to = obj_grid_location.x + obj_size.width
        var y_from = obj_grid_location.y
        var y_to = obj_grid_location.y + obj_size.height
        # check part of grid for obj, and remove it from there
        for c in range(x_from, x_to):
            for r in range(y_from, y_to):
                if (c < Constants.NUM_COLUMNS and c >= 0 and r < Constants.NUM_ROWS and r >= 0):
                    for obj_in_cell in Utilities.Cells[c][r]:
                        if (obj == obj_in_cell.obj):
                            Utilities.Cells[c][r].erase(obj_in_cell)
        pass

# -----------------------------------------------------
# ------------- Collision Detection Class -------------
# -----------------------------------------------------

class Collision_Detector:
    var obj = null                      # object to check its surroundings
    var OBJ_Location = Vector2()        # object's location on grid
    var OBJ_Size = Vector2()            # objects size in grid units (pixels divided by grid size)
    var offset = Vector2()              # offset of object's original location for collision calculations
    var end_location = Vector2()        # we check collisions in the outline of this location
    var results = []                    # list of found objects lying in collision cells
    var blocking_objects_types = []     # list of object types that blocks movement
    # ---------------------------
    func _init(var object, var list):
        obj = object
        update_object_size()
        for item in list:
            blocking_objects_types.push_back(item)
        update_location()
        pass
    # ---------------------------
    func update_location():
        OBJ_Location = obj.get_pos().snapped(Constants.vGRID) / Constants.GRID
        pass
    # ---------------------------
    func update_object_size():
        OBJ_Size = obj.get_object_size() / Constants.GRID
        pass
    # ---------------------------
    # evaluate if should obj stop
    func should_stop(var dir):
        var results = check_direction(dir)
        # These are all objects that are solid and any moving object should
        # be blocked when trying to move through them.
        for result in results:
            if (blocking_objects_types.has(result.type) and result.obj != obj):
                return true
        pass
    # ---------------------------
    func check_direction(var dir):
        update_location()
        results.clear()

        offset = dir * OBJ_Size
        if (offset.x < -1):
            offset.x = -1
        if (offset.y < -1):
            offset.y = -1
        end_location = OBJ_Location + offset

        if (dir.x != Constants.CENTER and dir.y != Constants.CENTER):
            if (end_location.x >= 0 and end_location.x < Constants.NUM_COLUMNS and
                end_location.y >= 0 and end_location.y < Constants.NUM_ROWS):
                    for obj_in_cell in Utilities.Cells[end_location.x][end_location.y]:
                        results.push_back(obj_in_cell)
        elif (dir.x != Constants.CENTER):
            if (end_location.x >= 0 and end_location.x < Constants.NUM_COLUMNS):
                for y in range(OBJ_Size.height):
                    if (OBJ_Location.y + y >= 0 and OBJ_Location.y + y < Constants.NUM_ROWS):
                        for obj_in_cell in Utilities.Cells[end_location.x][OBJ_Location.y + y]:
                            results.push_back(obj_in_cell)
        elif (dir.y != Constants.CENTER):
            if (end_location.y >= 0 and end_location.y < Constants.NUM_ROWS):
                for x in range(OBJ_Size.width):
                    if (OBJ_Location.x + x >= 0 and OBJ_Location.x + x < Constants.NUM_COLUMNS):
                        for obj_in_cell in Utilities.Cells[OBJ_Location.x + x][end_location.y]:
                            results.push_back(obj_in_cell)
        return results
        pass
RebelliousX commented 7 years ago

Here is how it looks like in actual example. I created a node that draws semi transparent white rectangle for any cell in collision grid that has the player object or a moving platformer. :) You can see the hero can't pass when laser is on, but can when it is off. I decided to allow him to go over a moving platformer, but he should die if he was hit horizontally. All these checks are not part of collision detection, but the node of the hero himself. And as you can tell, he is not dead since the game is not finished :p

Please don't mind the low framerate, it was hard enough to get it under 10MB for github to allow me to upload this :p

castle 3

I apologize to @eon-s for hijacking his issue.

blurymind commented 7 years ago

Ah i see.. your example is for platformer. My function only works for top down movement

On 12 Apr 2017 12:41, "Thaer Razeq" notifications@github.com wrote:

Here is how it looks like in actual example. I created a node that draws semi transparent white rectangle for any cell in collision grid that has the player object or a moving platformer. :) You can see the hero can't pass when laser is on, but can when it is off. I decided to allow him to go over a moving platformer, but he should die if he was hit horizontally. All these checks are not part of collision detection, but the node of the hero himself.

Please don't mind the low framerate, it was hard enough to get it under 10MB for github to allow me to upload this :p

[image: castle 3] https://cloud.githubusercontent.com/assets/1888186/24955839/b7cb23ca-1f4a-11e7-91b5-23be646196fa.gif

β€” You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/godotengine/godot/issues/7130#issuecomment-293551538, or mute the thread https://github.com/notifications/unsubscribe-auth/AGMbVQTdL5Re4LfM9MZrST6TeXnqsz70ks5rvLiHgaJpZM4K0CS3 .

eon-s commented 7 years ago

@RebelliousX is ok :smile: , shows the complexity and dynamism of grid based movements (something I always try to do on every engine), I think it escapes of what a single body can do.

RebelliousX commented 7 years ago

@blurymind You are mistaken if you think the code above is only for a platformer. It can be used for any grid movement even RPG or 3D Chess board if you ignore the Z-Dimension. :) But if your code works for you, it certainly is shorter and that is great, you wrote it and you understand it better than anyone. πŸ‘

@eon-s You see, It is not really complicated once you know the design principle behind it. For me, I thought of this long, long time ago (many years ago), but the idea faded because I didn't think about it twice and thought it was silly and never tried to write single line of code. But after seeing The aforementioned youtube videos (especially the first one by GDquest, about the design analysis) I didn't have to look for code, since it was really trivial to implement. πŸ™‚

eon-s commented 6 years ago

Now I don't think that adding more things to kinematic body will be a good idea, is already too bloated, maybe one day on a character controller node but not on the poor kinematic body.

And relative movement may be added by #11071

Closing this now because I will forget later.