My first action RPG game in godot.
-
2D, Pixel preset -- set as default texture for imports
-
Project Settings > Display > Window >
Size > Width = 320
Size > Height = 180
Size > Test Width = 1280
Size > Test Height = 720
Stretch > Mode = 2D
Stretch > Aspect = keep
-
res://World.tscn
- Create a new 2D node calledWorld
. -
Player
of typeKinematicBody2D
.- Create a
Sprite
with textureres://Player/player.png
. - Since, the
Player.png
file contains a lot of frames we need to adjust theanimation
properties for the player sprite. Set thehframes
property accordingly. In our case there are 60 horizontal frames. - Attach a script to the player node.
- Create a
Code for calculating the direction unit vector.
var direction_unit_vector := Vector2(
Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left"),
Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
).normalized()
Frame independent change in velocity:
export(float) var MAX_SPEED := 2.5
export(float) var ACCELERATION_MAGNITUDE := 10.0
export(float) var DEACCELERATION_MAGNITUDE := 15.0
var velocity = Vector2.ZERO
func _physics_process(dt: float) -> void:
var direction_unit_vector := Vector2(
Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left"),
Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
).normalized()
if direction_unit_vector != Vector2.ZERO:
velocity = velocity.move_toward(direction_unit_vector * MAX_SPEED, ACCELERATION_MAGNITUDE * dt)
else:
velocity = velocity.move_toward(Vector2.ZERO, DEACCELERATION_MAGNITUDE * dt)
var obj : KinematicCollision2D = move_and_collide(velocity)
if (obj != null):
velocity = obj.collider_velocity
Refer Chapter 4 of https://salmanisaleh.files.wordpress.com/2019/02/fundamentals-of-physics-textbook.pdf
dv = a dt
=> v = a (t2 - t1) + constant
OR
=> final velocity = initial velocity + acceleration * delta-time
First step is to add a CollisionShape2D
for the movement to the player node. It should look something like this:
-
Bush
StaticBody2D
Sprite
-
Change type of World node to ysort.
Use of AnimationPlayer
node. We will use sprite's hframes as key frames for the animation.
These are 4 basic movement animations and 4 basic idle animations that is needed:
- run_right & idle_right
- run_left & idle_left
- run_up & idle_up
- run_down & idle_down
Then we will create a function called _update_player_animation()
that will be called from _physics_process()
after calculating the direction vector from input strength to update the animation.
enum State {
IDLE_RIGHT
RUN_RIGHT,
IDLE_UP,
RUN_UP,
IDLE_LEFT,
RUN_LEFT,
IDLE_DOWN,
RUN_DOWN
}
const state_to_animation : Dictionary = {
State.IDLE_RIGHT : "idle_right",
State.RUN_RIGHT : "run_right",
State.IDLE_UP : "idle_up",
State.RUN_UP : "run_up",
State.IDLE_LEFT : "idle_left",
State.RUN_LEFT : "run_left",
State.IDLE_DOWN : "idle_down",
State.RUN_DOWN : "run_down"
}
var velocity = Vector2.ZERO
onready var animation_player : AnimationPlayer = $AnimationPlayer
onready var player_state = State.IDLE_RIGHT setget set_player_state, get_player_state
func set_player_state(movement_direction : Vector2):
if (movement_direction.x > 0):
player_state = State.RUN_RIGHT
elif (movement_direction.x < 0):
player_state = State.RUN_LEFT
elif (movement_direction.y > 0):
player_state = State.RUN_DOWN
elif (movement_direction.y < 0):
player_state = State.RUN_UP
else:
player_state = State.IDLE_RIGHT
func get_player_state():
return player_state
func _update_player_animation() -> void:
animation_player.play(state_to_animation[player_state])
Here is the basic movement demo:
Animation in each direction can be elegantly coded with an AnimationTree
node to replace the code written in previous chapter.
Therefore, create a new child node AnimationTree
for Player
and set the anim_player
property to AnimationPlayer
node and tree_root
to a AnimationNodeStateMachine
and enable the active
property.
Then add 2 BlendSpace2D
for Idle and Run.
onready var animation_player : AnimationPlayer = $AnimationPlayer
onready var animation_tree : AnimationTree = $AnimationTree
onready var animation_state = animation_tree.get("parameters/playback")
func _physics_process(dt: float) -> void:
var direction_unit_vector := Vector2(
Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left"),
Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
).normalized()
if direction_unit_vector != Vector2.ZERO:
animation_tree.set("parameters/Idle/blend_position", direction_unit_vector)
animation_tree.set("parameters/Run/blend_position", direction_unit_vector)
animation_state.travel("Run")
velocity = velocity.move_toward(direction_unit_vector * MAX_SPEED, ACCELERATION_MAGNITUDE * dt)
else:
animation_state.travel("Idle")
velocity = velocity.move_toward(Vector2.ZERO, DEACCELERATION_MAGNITUDE * dt)
var obj : KinematicCollision2D = move_and_collide(velocity)
if (obj != null):
velocity = obj.collider_velocity
Change the type of World
node back to node 2D and create a separate child YSort
node and drag the player and bush nodes as child of YSort
.
First task is to set the background grass. There are 2 methods to add a texture background.
- By using the sprite texture region method (prefer this method)
- By using TextureRect (not preferred)
Add a new TileMap
node and set autotiles. Use the following bitmap mask.
Finally draw some tiles on the level.
Create another autotile set for cliff. Call it CliffTileMap
.
Next step is to set some collisions with tile map.
The bitmask for tile set follows same pattern. Then set the collision shapes to the autotiles to enable the collisions.
Here is the demo:
Create 4 basic attack animations and add them to the animation tree in a BlendShape2D
node.
Also, add animation callbacks to attack_move_finished()
function to change the player state variable. (refer the code)
enum State {
MOVE,
ROLL,
ATTACK
}
onready var animation_player : AnimationPlayer = $AnimationPlayer
onready var animation_tree : AnimationTree = $AnimationTree
onready var animation_state = animation_tree.get("parameters/playback")
onready var player_state = State.MOVE
func _ready() -> void:
animation_tree.active = true
func _physics_process(dt: float) -> void:
match player_state:
State.MOVE:
move_state(dt)
State.ROLL:
pass
State.ATTACK:
attack_state()
func move_state(dt) -> void:
var direction_unit_vector := Vector2(
Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left"),
Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
).normalized()
if direction_unit_vector != Vector2.ZERO:
animation_tree.set("parameters/Idle/blend_position", direction_unit_vector)
animation_tree.set("parameters/Run/blend_position", direction_unit_vector)
animation_tree.set("parameters/Attack/blend_position", direction_unit_vector)
animation_state.travel("Run")
velocity = velocity.move_toward(direction_unit_vector * MAX_SPEED, ACCELERATION_MAGNITUDE * dt)
else:
animation_state.travel("Idle")
velocity = velocity.move_toward(Vector2.ZERO, DEACCELERATION_MAGNITUDE * dt)
var obj : KinematicCollision2D = move_and_collide(velocity)
if (obj != null):
velocity = obj.collider_velocity
if Input.is_action_just_pressed("attack"):
player_state = State.ATTACK
func attack_state() -> void:
velocity = Vector2.ZERO
animation_state.travel("Attack")
func attack_move_finished() -> void:
player_state = State.MOVE
First, make all the Bush
node the child of another YSort
node called Bushes
.
Create a grass scene from a Node2D
scene. Drop some grass to the world. And attach a script Grass.gd
to it.
Now we need to create a grass death effect from a Node2D
scene. To the grass death effect add animated sprite node with grass effect sprite sheet.
Then add code for instancing the grass effect on runtime in Grass.gd
.
func _physics_process(delta: float) -> void:
if Input.is_action_just_pressed("attack"):
var GrassEffectScene : PackedScene = load("res://Effects/GrassEffect.tscn")
var grass_effect = GrassEffectScene.instance()
var world = get_tree().current_scene
world.add_child(grass_effect)
grass_effect.global_position = global_position
queue_free()
The above code illustrates the instancing during runtime. The solution is not complete as the hurtboxes and hitboxes are not implemented yet.
First, create a following layers in the Project Settings > Layer Names > 2d Physics
.
Create 2 Area2D
scenes with a CollisionShape2D
child node with name Hurtbox
and Hitbox
.
Then drop a Hurtbox
instance to the Grass
scene and enable editable children and add a rectangle collision shape.
For, the player we need to add the Hitbox
scene which will be rotate when different animation is played.
Then key CollisionShape2D > rotation_degrees
property to beginning of each animation with appropriate rotation. For eg. here is for attack down.
Similarly, enable the collision shape at the middle of the attack, and disable the collision shape at the end of the attack. (By default, keep it disabled.)
Finally set the appropriate collision_mask
and collision_layer
property for different nodes.
Similiar to previous chapter add 4 basic roll animations along with a BlendSpace2D
which is connected to Idle
state.
Also, created a callback function roll_animation_finished()
.
Then add code to change the blend_position
using direction_vector
.
animation_tree.set("parameters/Roll/blend_position", direction_unit_vector)
Define a new function called roll_state()
.
Create a KinematicBody2D
and name it Bat
.
Add a Shadow
sprite node and an AnimatedSprite
for wings flap animation.
Add CollisionShape2D
near the shadow which will collide with player's foot collision shape.
Rename the 5th 2D physics layer as Enemy
in the project settings.
Then change the bat collision_layer
property to Enemy
layer.
Then add a Hurtbox
with capsule collision shape.
Change the Hurtbox > collision_layer
to EnemyHurtbox
layer.
Then connect the area_entered(area:Area2D)
signal for Hurtbox
.
Let's add basic code for knocking the bat in right direction (+ve x axis direction).
export(float) var MAX_SPEED := 1
export(float) var ACCELERATION_MAGNITUDE := 10
export(float) var KNOCKBACK_MULTIPLIER := 3.5
var knockback_velocity:Vector2 = Vector2.ZERO
func _physics_process(dt: float) -> void:
knockback_velocity = knockback_velocity.move_toward(Vector2.ZERO, ACCELERATION_MAGNITUDE * dt)
var obj : KinematicCollision2D = move_and_collide(knockback_velocity)
if (obj != null):
knockback_velocity = obj.collider_velocity
func _on_Hurtbox_area_entered(area: Area2D) -> void:
knockback_velocity = Vector2.RIGHT * KNOCKBACK_MULTIPLIER
Let's modify the above code for knocking back to any of the 4 direction (UP, DOWN, LEFT, RIGHT). To do this we need to store the sword hit knockback vector and update this exactly similar to roll direction vector.
Then the _on_Hurtbox_area_entered(area)
function will be modified like this:
func _on_Hurtbox_area_entered(area: Area2D) -> void:
if area.knockback_vector:
knockback_velocity = area.knockback_vector * KNOCKBACK_MULTIPLIER
Knockback Demo:
Now, let's add the enemy health stats.
Create a new basic scene from Node
and name it Stats
.
Attach a script to Stats
scene.
# File: Stats.gd
extends Node
export(int) var MAX_HEALTH := 1
onready var health : int = MAX_HEALTH setget set_health
signal no_health
func set_health(value) -> void:
health = value
if health == 0:
emit_signal("no_health")
Then attach a signal handler for no_health
in Bat.gd
. Also, modify the _on_Hurtbox_area_entered(area)
signal handler to reduce the health of enemy.
func _on_Hurtbox_area_entered(area: Area2D) -> void:
if area.knockback_vector:
stats.health -= 1
knockback_velocity = area.knockback_vector * KNOCKBACK_MULTIPLIER
func _on_Stats_no_health() -> void:
queue_free()
Damage variable for sword. Attach a script to Hitbox
and export a damage variable. Then extend this code in sword hitbox code.
Then finally, use this damage var to update the health.
We need to start with creating a PlayerDetectionZone
scene which will extend the Area2D
node and has a child CollisionShape2D
node.
Attach signal handlers for body_entered(body)
and body_exited(body)
.
Clear all bits for collision_layer
and collision_mask
property. Then set the player
layer bit for collision_mask
.
Then add this as child of the Bat
and add a Circle
shaped collision shape.
Defining three states for bat: IDLE
, WANDER
and BAT
.
Then add the necessary code for following the player.
Demo:
Then setup the player stats.
Demo: