123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- Kinematic Character (2D)
- ========================
- Introduction
- ~~~~~~~~~~~~
- | Yes, the name sounds strange. "Kinematic Character" WTF is that? The
- reason is that when physics engines came out, they were called
- "Dynamics" engines (because they dealt mainly with collision
- responses). Many attempts were made to create a character controller
- using the dynamics engines but it wasn't as easy as it seems. Godot
- has one of the best implementations of dynamic character controller
- you can find (as it can be seen in the 2d/platformer demo), but using
- it requieres a considerable level of skill and understanding of
- physics engines (or a lot of patience with trial and error).
- | Some physics engines such as Havok seem to swear by dynamic character
- controllers as the best alternative, while others (PhysX) would rather
- promote the Kinematic one.
- So, what is really the difference?:
- - A **dynamic character controller** uses a rigid body with infinite
- inertial tensor. Basically, it's a rigid body that can't rotate.
- Physics engines always let objects collide, then solve their
- collisions all together. This makes dynamic character controllers
- able to interact with other physics objects seamlessly (as seen in
- the platformer demo), however these interactions are not always
- predictable. Collisions also can take more than one frame to be
- solved, so a few collisions may seem to displace a tiny bit. Those
- problems can be fixed, but require a certain amount of skill.
- - A **kinematic character controller** is assumed to always begin in a
- non-colliding state, and will always move to a non colliding state.
- If it starts in a colliding state, it will try to free itself (like
- rigid bodies do) but this is the exception, not the rule. This makes
- their control and motion a lot more predictable and easier to
- program. However, as a downside, they can't directly interact with
- other physics objects (unless done by hand in code).
- This short tutorial will focus on the kinematic character controller.
- Basically, the oldschool way of handling collisions (which is not
- necessarily simpler under the hood, but well hidden and presented as a
- nice and simple API).
- Fixed Process
- ~~~~~~~~~~~~~
- To manage the logic of a kinematic body or character, it is always
- advised to use fixed process, which is called the same amount of times
- per second, always. This makes physics and motion calculation work in a
- more predictable way than using regular process, which might have spikes
- or lose precision is the frame rate is too high or too low.
- ::
- extends KinematicBody2D
- func _fixed_process(delta):
- pass
- func _ready():
- set_fixed_process(true)
- Scene Setup
- ~~~~~~~~~~~
- To have something to test, here's the scene (from the tilemap tutorial)
- attachment:kbscene.zip. We'll be creating a new scene for the character.
- Use the robot sprite and create a scene like this:
- .. image:: /img/kbscene.png
- Let's add a circular collision shape to the collision body, create a new
- CircleShape2D in the shape property of CollisionShape2D. Set the radius
- to 30:
- .. image:: /img/kbradius.png
- **Note: As mentioned before in the physics tutorial, the physics engine
- can't handle scale on most types of shapes (only collision polygons,
- planes and segments work), so always change the parameters (such as
- radius) of the shape instead of scaling it. The same is also true for
- the kinematic/rigid/static bodies themselves, as their scale affect the
- shape scale.**
- | Now create a script for the character, the one used as an example
- above should work as a base.
- | Finally, instance that character scene in the tilemap, and make the
- map scene the main one, so it runs when pressing play.
- .. image:: /img/kbinstance.png
- Moving the Kinematic Character
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Go back to the character scene, and open the script, the magic begins
- now! Kinematic body will do nothing by default, but it has a really
- useful function called
- `move(motion\_vector:Vector2) <https://github.com/okamstudio/godot/wiki/class_kinematicbody2d#move>`__.
- This function takes a
- `Vector2 <https://github.com/okamstudio/godot/wiki/class_vector2>`__ as
- an argument, and tries to apply that motion to the kinematic body. If a
- collision happens, it stops right at the moment of the collision.
- So, let's move our sprite downwards until it hits the floor:
- ::
- extends KinematicBody2D
- func _fixed_process(delta):
- move( Vector2(0,1) ) #move down 1 pixel per physics frame
- func _ready():
- set_fixed_process(true)
- | The result is that the character will move, but stop right when
- hitting the floor. Pretty cool, huh?
- | The next step will be adding gravity to the mix, this way it behaves a
- little more like an actual game character:
- ::
- extends KinematicBody2D
- const GRAVITY = 200.0
- var velocity = Vector2()
- func _fixed_process(delta):
- velocity.y += delta * GRAVITY
- var motion = velocity * delta
- move( motion )
- func _ready():
- set_fixed_process(true)
- Now the character falls smoothly. Let's make it walk to the sides, left
- and right when touching the directional keys. Remember that the values
- being used (for speed at least) is pixels/second.
- This adds simple walking support by pressing left and right:
- ::
- extends KinematicBody2D
- const GRAVITY = 200.0
- const WALK_SPEED = 200
- var velocity = Vector2()
- func _fixed_process(delta):
- velocity.y += delta * GRAVITY
- if (Input.is_action_pressed("ui_left")):
- velocity.x = -WALK_SPEED
- elif (Input.is_action_pressed("ui_right")):
- velocity.x = WALK_SPEED
- else:
- velocity.x = 0
- var motion = velocity * delta
- move( motion )
- func _ready():
- set_fixed_process(true)
- And give it a try.
- Problem?
- ~~~~~~~~
- And.. it doesn't work very well. If you go to the left against a wall,
- it gets stuck unless you release the arrow key. Once it is on the floor,
- it also gets stuck and it won't walk. What is going on??
- The answer is, what it seems like it should be simple, it isn't that
- simple in reality. If the motion can't be completed, the character will
- stop moving. It's as simple as that. This diagram should illustrate
- better what is going on:
- .. image:: /img/motion_diagram.png
- Basically, the desired motion vector will never complete because it hits
- the floor and the wall too early in the motion trajectory and that makes
- it stop there. Remember that even though the character is on the floor,
- the gravity is always turning the motion vector downwards.
- Solution!
- ~~~~~~~~~
- The solution? This situation is solved by "sliding" by the collision
- normal. KinematicBody2D provides two useful functions:
- - `KinematicBody2D.is\_colliding() <https://github.com/okamstudio/godot/wiki/class_kinematicbody2d#is_colliding>`__
- - `KinematicBody2D.get\_collision\_normal() <https://github.com/okamstudio/godot/wiki/class_kinematicbody2d#get_collision_normal>`__
- So what we want to do is this:
- .. image:: /img/motion_reflect.png
- | When colliding, the function move() returns the "remainder" of the
- motion vector. That means, if the motion vector is 40 pixels, but
- collision happened at 10 pixels, the same vector but 30 pixels long is
- returned.
- | The correct way to solve the motion is, then, to slide by the normal
- this way:
- ::
- func _fixed_process(delta):
- velocity.y += delta * GRAVITY
- if (Input.is_action_pressed("ui_left")):
- velocity.x = - WALK_SPEED
- elif (Input.is_action_pressed("ui_right")):
- velocity.x = WALK_SPEED
- else:
- velocity.x = 0
- var motion = velocity * delta
- motion = move( motion )
- if (is_colliding()):
- var n = get_collision_normal()
- motion = n.slide( motion )
- velocity = n.slide( velocity )
- move( motion )
- func _ready():
- set_fixed_process(true)
- | Note that not only the motion has been modified but also the velocity.
- This makes sense as it helps keep
- | the new direction too.
- The normal can also be used to detect that the character is on floor, by
- checking the angle. If the normal points up (or at least, within a
- certain threshold), the character can be determined to be there.
- A more complete demo can be found in the demo zip distributed with the
- engine, or in the
- https://github.com/okamstudio/godot/tree/master/demos/2d/kinematic\_char.
- *Juan Linietsky, Ariel Manzur, Distributed under the terms of the `CC
- By <https://creativecommons.org/licenses/by/3.0/legalcode>`__ license.*
|