How to Make Your Player Swim in Godot 3.1

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
In this video, you’ll learn how to implement a swimming mechanic. This particular style of swimming will cause the player to float up towards the surface without being pushed out of the water. You’ll also learn how to alter the character’s movement underwater to better convey the feeling that they’re swimming. Howdy, and welcome to Game Endeavor. Learn practical dev skills and improve the quality of your games by subscribing and ding-a-linging that bell. There are many different ways to handle swimming in a platforming game. With the style that I’ll be showing you in this video, the player will passively float upwards, but will be able to directly control their movement using the x and y axis controls, or even the jump button if you prefer. To keep the player controller from becoming a confusing mess of spaghetti code, we’ll be using a state machine to manage the flow of the logic. I have a short video on creating a simple state machine if you haven’t brought your own, iCard in the corner. Once you’re ready, let’s go ahead and get started. How you choose to implement water in your game can be as varied as any other choice you will make as a developer. Each with their own advantages and disadvantages. For this tutorial, I will be using a TileMap to layout collisions and sprites for the water. Alternatively you can use Area2Ds if you prefer. In my TileMap, I have created my water tiles, and they have collisions. The player will not be colliding with the collisions, but we will be needing them to detect the water. The TileMap will be on its own collision layer that I have designated as water. To determine whether or not our character should be swimming, we need something to detect the TileMap. We’ll be checking a specific point for water collision, and we’ll need a way to designate the point that we’re going to check. For this I’m going to use a Position2D node named SwimLevel. The y location of this node will determine where the water comes up to the player. Consider this while making your swimming animations. Inside of our player script, we can go ahead and cache the node into an onready variable named `swim_level`. Probably the most important part of adding a swim mechanic, is being able to determine whether or not the character is in water. We’re going to create a simple little function that will check for this, which will be called `is_in_water()`. In this method, we’re going to do something similar to a raycast, except we’re only going to check a specific point. So to do this we’re going to need the world’s direct space state, which we’ll get by saying `var space_state = get_world_2d().direct_space_state` So far on the channel, I’ve shown you how to use the space state to perform raycasts and shape intersections, but there’s another useful option that the space state provides us with. That being the intersect_point which will return an array of collisions at a given point. We’ll call it by saying `var results = space_state.intersect_point(` The first parameter is going to be the point where the space state checks for collisions. We’re checking if we’re in water, so we’re going to use `swim_level.global_position` as the point. The next parameter is the maximum number of results you want this to return. We only need one, because there’s either water, or there isn’t. The next parameter is an array of what you want to exclude from the cast, which we don’t need, so we’ll pass in an empty array. And then we have to pass in what collision layer we want to mask for when performing the point intersection. This will be what collision layer you’ve set for your water. Personally I don’t want to use a magic number here. I would like to be able to see what layer I’m referencing. What I generally do with collisions is to make a script that stores them for me. So I’m going to make a script called `CollisionLayers` giving it a class name so that we can reference it anywhere in our project. We don’t need to autoload it, since we’re only going to be storing constants here. And the way we’re going to set this up is by creating some constants for whatever collision layers we’ve defined in our game. We can use binary shifters to keep our values neat and clean and easy to maintain. When using binary shifters, the value on the right is what’s used to determine the layer of the collision. You can get this value by going to your inspector, finding the collision_layer property, hovering over the layer you want, and using the `Bit` value. You’ll notice that this increments by 1 from 0 to 31. Which fun fact, using this method you can get an extra 12 layers for a total of 32 as opposed to the 20 that you’re limited to by the inspector. A little trick you can use if you ever run out of layers. With that out of the way, we can return to the method in our player script and plug it in by passing `CollisionLayers.WATER` as a parameter. If you’re using an Area2D for your water instead of a TileMap like I am, then this method has two more additional parameters for whether or not you want to detect bodies and areas. These two lines of code will be used twice more in this video, so I’m going to go ahead and copy them just to make this whole tutorial making process easier for myself. And then to finish off this function, we want to `return results.size() != 0` which will return true if we detected water, false if not. This is what we’ll use to enter and exit the swim state. Now we need to handle the logic for swimming. We’ll start by moving our character along the y axis. To do this I’m going to be using three different speeds. One for passively moving upwards, one for actively moving upwards, and another for actively moving downwards. We’re going to store these as constants. One being `const SWIM_UP_SPEED` which I will set to -3 * 96. 96 being the size of my tileset. This means that at max speed, the character will move 3 tiles upward per second. Next we’ll define `const PASSIVE_SWIM_UP_SPEED` which I will set to -1 * 96. And then we’ll define `const SWIM_DOWN_SPEED` and I will set mine to 3 * 96. 3 being positive, which will cause the character to move downwards. Once we start plugging logic into our state machine, we’re also going to modify our horizontal speed for swimming, so while we’re here, we’re going to create another const named `SWIM_SPEED_HORIZONTAL` which I will set to 3 * 96. Rather than applying gravity to our player while swimming, we’re going to apply a more linear velocity to them, but based on their input. We’re going to create a function for handling this called `_apply_vertical_swim_velocity():`. Which will have an argument named delta. We’ll start off by getting the player’s vertical input. If you’ve followed along with the platformer series, we’re going to do this much like we got our horizontal input by saying `var input = -int(Input.is_action_pressed(“move_up”) || Input.is_action_pressed(“jump”)) + int(Input.is_action_pressed(“move_down”))`. This will set input to a value of -1 if the player is pressing up or jump, 0 if there is no input or they’re pressing both up or jump and down, and 1 if they’re pressing down. Next we’ll define a variable for how fast the player should move along the y axis, which we’ll name `y_speed`. We’re going to initialize this to `PASSIVE_SWIM_UP_SPEED`, which is what it will be if there’s no input. Then we can check `if input < 0:` meaning that the player is pressing up, then we’ll set y_speed to `SWIM_SPEED_UP`. `elif input > 0:` meaning the player is pressing down. Then we’ll set y_speed to `SWIM_SPEED_DOWN`. This is the maximum speed to move the player along the y axis. If we were to set this value immediately then it wouldn’t have much of an underwater feel. So instead we’re going to lerp towards this value so that it gradually gets set. We’ll say `velocity.y = lerp(velocity.y, y_speed`. We’re going to use a somewhat low weight here, being 0.75. Since we’re using lerp again, allow me to address something that was pointed out to me some time ago. When using lerp like this, it doesn’t take delta into consideration. Meaning that if the game were to lag, then this will quickly become inaccurate. However, it turns out that if we apply delta to the weight, we can fix this. But as you can imagine, this will drastically alter how the weight behaves, because delta is generally a small fraction, typically 1/60th. We can fix this by dividing by 1/60th, ensuring to make the 60 a float. I’m using 1/60th here because ideally my game will run at 60 frames per second. If you’re running your game at a different ideal frame rate, then you will want to use a different value. With this knowledge at your disposal, you should apply it to your horizontal lerping as well. I am using `move_and_slide_with_snap()` for my movement. To handle the snap vector, I am setting it to some downward Vector2 if the character is not jumping, otherwise I set it to Vector2.ZERO. I have a variable for this called `is_jumping` which I set to true when the character jumps, and false otherwise. This is what keeps Paw Bearer stuck to the floor when moving along slopes and moving platforms, and it also turns the ground into a death trap underwater. We need to adjust is_jumping for our underwater needs. I’m going to assume that we don’t need this while underwater, and just set is_jumping to true. This will get our character moving how we want, but there’s an issue we need to address first. This will push our character up towards the surface, and eventually out of the water to leave the swim state, only to fall back into the water, and repeat. We need a way to prevent the player from getting pushed completely out of the water. We’re going to create a function for handling this named `_handle_surfacing()` and it will need delta as an argument as well. We’ll say `if velocity.y < 0` because we only want to perform this logic whenever we’re moving upwards, since there’s no point in doing it downwards. We need to detect the surface, but we can’t just cast a ray upwards because you may be using a TileMap to lay out your water. In which case, casting a ray upwards would detect each water tile, despite being deep underwater. Instead we’re going to look to where the player is going to be when they move next. If there isn’t water there, then we’ll cast an arrow down towards the player to find the surface. Remember the code snippet that we copied earlier? We’re going to paste it here to do another `intersect_point()`. We need to change where we cast the point though, because we want to cast where the player will be once they move. So we’ll create a variable named `surface_level`. We’re going to set this to the swim_level’s global_position like before, but we’re going to add `Vector2.DOWN * velocity.y * delta` to it, which is the distance the player will move upward if unaltered. Then we’ll pass our new `surface_level` variable into the `intersect_point()` method. We want to know if there isn’t water here, so we’ll say `if !results:`. After which we’ll want to cast our raycast, so we’ll say `var ray_result = space_state.intersect_ray(`. We want to cast from above the surface of the water down to our player, so we’ll pass in `surface_level` as the from vector. Then `swim_level.global_position` as our to vector. We don’t need any exceptions so we can pass in an empty array. Then finally we can use `CollisionLayers.WATER` as our collision mask. If you’re using areas instead of bodies for the water, then be sure to set it up here as well. This raycast will hit the surface of the water, and if we’re even casting it then it’s practically guaranteed to hit, but sometimes I can be a little too cautious, so I’m going to say `if ray_result:` juuust to make sure we don’t get any errors. The reason we’re casting this ray is to get the surface position of the water so that we can modify our character’s velocity so that they move directly to it and not past it. So we’ll go ahead and set our character’s y velocity. We’re going to get the displacement of the two positions by saying `(ray_result.position.y - swim_level.global_position.y) which is the distance along the y axis that the character needs to move in order to get there. However, when we call `move_and_slide()` later, it’s going to modify velocity by delta so that the movement is consistent over time. Thus we need to divide this displacement by delta, then sit back and giggle when Godot multiplies it by delta to get the number we originally wanted. This handles our movement along the vertical axis, and we’re almost ready to head into the state machine to connect up our logic. Right quick though let us deal with horizontal acceleration and deceleration as well, it won’t take but a second. If you follow along with the platformer series, then we use lerping for our horizontal movement. Specifically we have a `get_h_weight()` method to determine how we should be applying the weight to our lerping. Inside of this method, all we need to do is add a condition (which we will define in our state machine in a moment), saying `if state_machine.is_swimming()`. state_machine just being where we cached our state machine node. If this is true then we want to return a low value, something that feels somewhat sluggish but not unresponsive. I will be using 0.05. This should be before your `is_on_floor()` check so that it doesn’t get cancelled out by swimming along the bottom of the floor, and change that one to an elif if you’re particular. We can finally head on over to our state machine and start plugging in the logic. We’re going to be adding a new state, so `add_state(“swim”)`. We’re only adding one state because swimming up and down is easy to keep in one state at the moment, and I don’t want to over complicate it. Before I forget, let’s add in a method at the bottom of the script saying `func is_swimming():`. It just needs to `return state == states.swim`. We’ll be adding our logic to our `_state_logic()` method, so create a condition for it saying `elif state == states.swim:`. Inside of this if condition we want to say `parent._handle_movement(parent.SWIM_SPEED_HORIZONTAL)`. `_handle_movement()` is the method in my player script that I use for setting the movement along the y axis. It takes in a speed value which is what allows us to slow the player down while swimming. We’ll then say `parent._apply_vertical_swim_velocity(`. Pass in the delta parameter. And then `parent._handle_surfacing(delta)`. With the logic implemented, we then want to transition to and from the state. We’ll go into our `states.idle` statement and add a condition checking if `parent.is_in_water()`. If this is true, then we want to `return states.swim` so that the state machine will transition to our state. Copy this condition, because we also want to use it inside of our run and fall states. If you ever think the player can jump into water (such as if the water can rise up while they’re jumping) then you may want to add it to the jump state as well, go nuts. Transitioning out of the water is a little different however. First we’ll handle the simple condition, saying `if !parent.is_in_water():`. Then return states.idle. This happens if you have slopes or moving platforms in your water and they push your player out of the water. But perhaps you want to allow your player to jump out of the water when near the surface. Well we need a way to determine if they’re close enough to the surface. So quickly, run back to your player script, and we’ll make a function named `can_jump_out_of_water():`. Paste in the code from earlier, but to it we’re going to add a distance upward. This distance will be how close to the surface you want the player to be able to jump out of the water. I will use 32 pixels by saying `+ Vector2.UP * 32`. When returning whether or not we can jump, first we want to make sure that `velocity.y <= 0` meaning that the player isn’t swimming downwards. Feel free to remove this if your character is a skipping stone. Then we’ll add `&& results.size() == 0`, meaning that there is no water detected from the `intersect_point()`. We also need to set a velocity to our player so that they jump out of the water. So we’ll go to the top of our script and create a constant named `JUMP_OUT_VELOCITY` which I will set mine to -1200. If you want to learn how to set a specific height to this jump, then watch my video on how you can set a jump height and duration using equations of motion. We can go back to our state machine now and setup the transition that will allow us to jump out of the water, by saying: `elif (Input.is_action_pressed(“move_up”) || Input.is_action_pressed(“jump”)) && parent.can_jump_out_of_water():` This allows the player to press either of these keys to jump out of the water. You can use whichever you want or both. Since we’re only using one state for swimming and not several different states, we need to update our animations for swimming the old way. So at the bottom of my `_state_logic()` method, I’m going to say `if state == states.swim:` because we don’t want to call swim animations during the jump state. `parent._update_swim_animations()`. Which we’re going to create right now as we moonmosey back on over to our player script. We’ll create the method, saying `func _update_swim_animations():`. We’re going to be checking some conditions here to see what animation we should be using, which we’ll store in a variable named animation. I will default to “swim” so that it if none of these conditions are triggered, then the character will do normal swimming animation. We want to get the input that the player is pressing so that we can tell if the player is actively swimming upward. I’m just going to steal this from our `_apply_vertical_swim_velocity()` method and plop it down here. `if input < 0:` then the player is pressing up, so we should set animation to “swim_up” which is an animation I have defined for swimming upward. If you want to use a downward swimming animation then you would add another if condition checking if input > 0. I also have an animation for treading water that I would like to use, which is where the character is not moving and is floating near the surface. However due to issues with using lerping for movement in 3.1, checking if our velocity is zero will not return true. Instead we need to check `if abs(velocity.x) < 64.0` which will check if the character is moving slow enough for our liking. 64.0 is a variable of my choosing, you’ll need to play around with it for yourself and find what is best for your game. Then we’ll add to this `&& can_jump_out_of_water():` which will ensure that our character is close enough to the surface to jump. If this is true, then we can set `animation = “tread_water”` which is what I’ve named my animation. And then, once we’ve picked our chosen state, we can set it to the animation player, but first we’ll say `if animation_player.assigned_animation != animation` which will check that it’s not the current animation already, otherwise it will keep restarting the animation. Then `animation_player.play(animation)`. If you want to learn more practical dev skills, then watch this video on implementing wall jumping in your game, and if you’re new, then join the sub-club to get notified for future tutorials.
Info
Channel: Game Endeavor
Views: 6,923
Rating: undefined out of 5
Keywords: Game Endeavor, GameEndeavor, rk, rk3Omega, Godot, Godot 3.0, Godot 3, Godot Engine, Godot Game Engine, Tutorial, Godot Tutorial, Godot Engine Tutorial, gdscript, 2D Game, Game Development, Game Creation, gamedev, Indie, Indie Development, Programming, Code, Coding, Scripting, swimming
Id: UL43DdnYMxg
Channel Id: undefined
Length: 18min 1sec (1081 seconds)
Published: Sat Dec 28 2019
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.