Mechanics for implementing a platformer on the Godot engine. Part 2

Hello, this is a continuation of the previous article on creating a playable character in GodotEngine. I finally figured out how to implement some of the mechanics, such as a second jump in the air, climbing up, and jumping off the wall. The first part was simpler in terms of saturation, since it was necessary to start with something in order to refine or redo it later.



Links to previous articles



To begin with, I decided to collect all the previous code so that those who used the information from the previous article would understand how I imagined the program in its entirety:



extends KinematicBody2D

# 
const GRAVITY: int = 40
const MOVE_SPEED: int = 120 #     
const JUMP_POWER: int = 80 #  

# 
var velocity: Vector2 = Vector2.ZERO

func _physics_process(_delta: float) -> void:
	#     
	move_character() #  
	jump()
	#     
	self.velocity.y += GRAVITY
	self.velocity = self.move_and_slide(self.velocity, Vector2(0, -1))

func move_character() -> void:
	var direction: float = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") 
	self.velocity.x = direction * MOVE_SPEED

func jump() -> void:
	if self.is_on_floor():
		if Input.is_action_pressed("ui_accept"): #     ui_accept
			#      
			self.velocity.y -= JUMP_POWER


I hope those who have read the previous article have approximately understood how everything works. Now let's get back to development.



State machine



The state machine (in my understanding) is a part of the program that determines the state of something: in the air, on the floor, on the ceiling, or on the wall, and also determines what should happen to the character in one place or another. GodotEngine has such a thing as enum, which creates an enumeration, where each element is a constant specified in the code. I think I'd better show this with an example:



enum States { #   States,       States.IN_AIR, States.ON_FLOOR...
	IN_AIR, #  
	ON_FLOOR, #   
	ON_WALL #  
}


This code can be safely put at the very beginning of the script of the game character and keep in mind that it exists. Next, we initialize the variable in the right place var current_state: int = States.IN_AIR, which is equal to zero if we use print. Next, you need to somehow determine what the player will do in the current state. I think many experienced developers who came from C ++ are familiar with the switch () {case:} construction. GDScript has a similar adapted construct, although switch is also on the agenda. The construction is called match.



I think it would be more correct to show this construction in practice, since it will be more difficult to tell than to show:



func _physics_process(_delta: float) -> void:
	#   
	match (self.current_state):
		States.IN_AIR:
			#      .
			self.move_character()
		States.ON_FLOOR:
			#  ,    .
			self.move_character()
			self.jump()
		States.ON_WALL:
			#  ,  ,      .     .
			self.move_character()
	#    


But we still don't change the state. We need to create a separate function, which we will call before the match, in order to change the current_state variable, which should be added to the code to the rest of the variables. And we'll call the function update_state ().



func update_state() -> void:
	#        .
	if self.is_on_floor():
		self.current_state = self.States.ON_FLOOR
	elif self.is_on_wall() and !self.is_on_floor():
		#     .
		self.current_state = self.States.ON_WALL
	elif self.is_on_wall() and self.is_on_floor():
		#  .      .
		self.current_state = self.States.ON_WALL
	else: #       
		self.current_state = self.states.IN_AIR


Now that the state machine is ready, we can add a ton of functions. Including adding animations to the character ... Not even that ... We can add a ton of animations to the character. The system has become modular. But we are not finished with the code here. I said at the beginning that I would show you how to do an extra jump in the air, climb up, and jump off the wall. Let's start in order.



Additional jump in the air



First, add the jump call in the States.IN_AIR state to our match, which we'll tweak a bit.



Here is the code for our jump that I fixed:



func jump() -> void:
	#    .    .
	if Input.is_action_pressed("ui_accept"): #    
		if self.current_state == self.States.ON_FLOOR:
			#  ,     _
			self.velocity.y -= JUMP_POWER
		elif (self.current_state == self.States.IN_AIR or self.current_state == self.States.ON_WALL)
				and self.second_jump == true:
				#             
			self.velocity.y = -JUMP_POWER
			#       
			self.second_jump = false
			#    var second_jump: bool = true   .   update_state()
			#   if self.is_on_floor(): self.second_jump = true #        .


In the comments to the code, in principle, it is said how I changed the program, I hope you understand my words there. But in fact, these fixes are enough to change the jump mechanics and improve to double. It took me a couple of months to invent the following methods. I actually wrote them the day before yesterday, October 1, 2020.



Climbing the walls



Unfortunately for us, the GodotEngine Wall Normal does not allow us to know, which means that we will have to create a small crutch. To begin with, I'll make a footnote of the currently available variables so that you can easily tell what has changed.



extends KinematicBody2D

# 
signal timer_ended #     yield  wall_jump,     .
# 
const GRAVITY: int = 40
const MOVE_SPEED: int = 120 #     
const JUMP_POWER: int = 80 #  
const WALL_JUMP_POWER: int = 60 #    .    
const CLIMB_SPEED: int = 30 #  

# 
var velocity: Vector2 = Vector2.ZERO
var second_jump: bool = true
var climbing: bool = false #   ,     ,  .
var timer_working: bool = false
var is_wall_jump: bool = false # ,  ,      
var left_pressed: bool = false #     
var right_pressed: bool = false #     
var current_state: int = States.IN_AIR
var timer: float = 0 #  ,     _process(delta: float)
var walls = [false, false, false] #    .    - .  - .
#      
# 
enum States {
	IN_AIR, #  
	ON_FLOOR, #   
	ON_WALL #  
}
#       ,   _process()   
func _process(delta: float):
	if timer_working:
		timer -= delta
	if timer <= 0:
		emit_signal("timer_ended")
		timer = 0


Now you need to determine which wall the player is climbing.



Here is the scene tree that you should prepare to implement the wall side qualifier.



image



Place 2 Area2Ds on the sides of the character and CollisionShape2D both should not overlap with the character. Sign the WallLeft / WallRight objects appropriately and attach the _on_body_endered and _on_body_exited signals to a single character script. Here is the code needed to define the walls (Add to the very end of the script):



#     
#  ,     
func _on_WallRight_body_entered(_body):
	if (_body.name != self.name):
		self.walls[0] = true #     ,   - 

func _on_WallRight_body_exited(_body):
	self.walls[0] = false #        -   

func _on_WallLeft_body_entered(_body):
	if (_body.name != self.name):
		self.walls[2] = true #     ,   - 

func _on_WallLeft_body_exited(_body):
	self.walls[2] = false #        -   


Let's start with the climbing method. The code will say everything for me

func climbing() -> void:
	if (self.walls[0] or self.walls[2]): #       
		#   action     ui_climb.        .
		self.climbing = Input.is_action_pressed("ui_climb")
	else:
		self.climbing = false


And we need to rewrite the move_character () control so that we can not just hold on, but climb up and down, since we have direction:



func move_character() -> void:
	var direction: float = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") 
	if !self.climbing:
		self.velocity.x = direction * MOVE_SPEED
	else:
		self.velocity.y = direction * CLIMB_SPEED


And we fix our _physics_process ():



func _physics_process(_delta: float) -> void:
	#   
	match (self.current_state):
		States.IN_AIR:
			self.move_character()
		States.ON_FLOOR:
			self.move_character()
			self.jump()
		States.ON_WALL:
			self.move_character()
	#     
	if !self.climbing:
		self.velocity.y += GRAVITY
	self.velocity = self.move_and_slide(self.velocity, Vector2(0, -1))


The character should now be able to climb walls.



Jump off the wall



Now let's implement the jump from the wall.



func wall_jump() -> void:
	if Input.is_action_just_pressed("ui_accept") and Input.is_action_pressed("ui_climb"): 
		#   1       
		self.is_wall_jump = true #     = 
		self.velocity.y = -JUMP_POWER #    -JUMP_POWER
		if walls[0]: #   
			self.timer = 0.5 #  self.timer  0.5 
			self.timer_enabled = true #  
			self.left_pressed = true #   left_pressed  
			yield(self, "timer_ended") #    timer_ended
			self.left_pressed = false #  left_pressed
		if walls[2]: #   
			self.timer = 0.5 #  self.timer  0.5 
			self.timer_enabled = true #  
			self.right_pressed = true #   right_pressed  
			yield(self, "timer_ended") #    timer_ended
			self.right_pressed = false #  right_pressed
		self.is_wall_jump = false # .    


We add a call to this method to our match -> States.ON_WALL and we attached our method to the rest of the _physics_process ().



Conclusion



In this article, I have shown the implementation of relatively complex mechanics (for beginners) in GodotEngine. But this is not the last part of a series of articles, so I ask those who know how to implement the methods I have shown in this article is better to write about them in the comments. I, and many readers, will be grateful for high-quality and quick solutions.



All Articles