Enemies - acbarker19/Godot-Action-RPG GitHub Wiki

How to Create the Enemy Bat

  1. Add a new KinematicBody2D
  2. Rename to Bat
  3. Add an AnimatedSprite as a child
  4. Create the sprite animation
  5. Move the Y offset to -12
  6. Add a Sprite for the shadow using a shadow texture
  7. Add a CollisionShape2D on the shadow of the bat
  8. Click on the Bat parent node
  9. Add an Enemy layer
  10. In the right menu, open Collision, set the Layer to Enemy, and set the Mask to World

Allow Enemy Bat to be Attacked

  1. Open the Bat scene
  2. [Add a hurtbox)(https://github.com/acbarker19/Godot-Action-RPG/wiki/Interactions#add-a-hitbox-or-hurtbox-to-an-object)
  3. Set the hurtbox's Layer and Mask
  4. Add a function when the bat is hurt

Add Knockback When the Enemy Bat is Attacked

Add the following code to the Bat script:

var knockback = Vector2.ZERO

func _physics_process(delta):
	knockback = knockback.move_toward(Vector2.ZERO, 200 * delta)
	knockback = move_and_slide(knockback)

func _on_Hurtbox_area_entered(area):
	knockback = area.knockback_vector * 120

Add a script to any hitbox that will knock back the bat with the following code (in this project, this is applied to the SwordHitbox):

var knockback_vector = Vector2.ZERO

Add the following code to any object that will control the hitbox (in this project, this is applied to the Player):

onready var objectHitbox = $DesiredHitboxForObject

func _ready():
	objectHitbox.knockback_vector = roll_vector

func move_state(delta):
	var input_vector = Vector2.ZERO
	// code to set input_vector...
	
	if input_vector != Vector2.ZERO:
		roll_vector = input_vector
		objectHitbox.knockback_vector = input_vector

How to Create Enemy Stats

  1. Create a new scene using the + icon near the top middle of the screen
  2. In the left menu, select Other Node and add a Node to the scene
  3. Save the scene as Stats.tscn
  4. Add a script to the Node
  5. Add the following code and anything additional that would apply to all enemies:
export(int) var max_health = 1
onready var health = max_health   // onready will make sure health is equal to whatever value is set for the scene that implements Stats rather than the initial max_health value of 1

Look in General Godot Information to see the explanation for export var

How to Add Stats to the Bat Enemy

  1. Open the Bat scene and select the Bat parent node
  2. In the left menu, click the Instance a scene file as a Node button (Looks like a chain)
  3. Select Stats
  4. Inside the Bat script, add the following:
onready var stats = $Stats
  1. In the scene editor, select the Stats node
  2. In the right menu, increase Script Variables > Max Health to the desired amount

How to Update Bat Health When Hit

  1. In the Bat script, add the following:
func _on_Hurtbox_area_entered(area):
	stats.health -= area.damage
  1. In the Stats script, add the following:
onready var health = max_health setget set_health

signal no_health

func set_health(value):
	health = value
	if (health <= 0):
		emit_signal("no_health")

func _onready():
	self.health = max_health
  1. Click on the Stats node withing the Bat scene
  2. In the right menu, open the Node tab and double click Stats.gd > no_health()
  3. In the popup, click Connect
  4. In the Bat script, add the following to the newly created signal function:
func _on_Stats_no_health():
	queue_free()

Visit the General Godot Information page for an explanation of setget

Add an Enemy State Machine

In the Bat script, add the following (only relevant code is shown):

export var ACCELERATION = 300
export var MAX_SPEED = 50
export var FRICTION = 200

enum {
	IDLE,
	WANDER,
	CHASE
}

var velocity = Vector2.ZERO

var state = CHASE

func _physics_process(delta):
	// knockback code
	
	match state:
		IDLE:
			pass
			
		WANDER:
			pass
			
		CHASE:
			pass

Make the Bat Detect and Chase the Player

  1. Create a PlayerDetectionZone
  2. In the Bat scene in the left menu, select Instance a scene file as a Node (looks like a chain)
  3. Select PlayerDetectionZone
  4. In the left menu, right click the PlayerDetectionZone and check Editable Children
  5. Select the PlayerDetectionZone's child CollisionShape2D
  6. In the right menu, choose a shape and set its size
  7. In the Bat script, add the following (only relevant code is shown):
onready var playerDetectionZone = $PlayerDetectionZone

func _physics_process(delta):
	// knockback code
	
	match state:
		IDLE:
			velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
			seek_player()
			
		WANDER:
			// wander code
			
		CHASE:
			var player = playerDetectionZone.player
			if player != null:
				var direction = global_position.direction_to((player.global_position))
				velocity = velocity.move_toward(direction * MAX_SPEED, ACCELERATION * delta)
			else:
				state = IDLE
			
	velocity = move_and_slide(velocity)

func seek_player():
	if playerDetectionZone.can_see_player():
		state = CHASE

Make the Bat Face the Correct Direction

  1. Open the Bat script
  2. Add the following:
onready var sprite = $AnimatedSprite

func _physics_process(delta):
	// knockback code
	
	match state:
		IDLE:
			// idle code
			
		WANDER:
			// wander code
			
		CHASE:
			// chase code
	
			sprite.flip_h = velocity.x < 0

Allow the Bat to Hurt the Player

  1. Open the Bat scene
  2. Add a Hitbox to the Bat
  3. In the right menu, select Collision > Mask > PlayerHurtbox

Signal to Parent Node as Health Changes

In Stats, add the following:

signal health_changed(value)

func set_health(value):
	health = value
	emit_signal("health_changed", health)

Signal to Parent Node as Max Health Changes

In Stats, add the following:

export(int) var max_health = 1 setget set_max_health

signal max_health_changed(value)

func set_max_health(value):
	max_health = value
	self.health = min(health, max_health)   // if hearts is greater than max_hearts, hearts is lowered
	emit_signal("max_health_changed", max_health)

Make an Enemy Wander

  1. Open the Bat scene
  2. Add a new Node2D called WanderController
  3. Right click on Wander Controller and select Save Branch as Scene
  4. Save the scene within the Enemies folder
  5. Open the WanderController scene and add a new Timer node
  6. In the right menu, enable Inspector > One Shot and Inspector > Autostart
  7. Attach a new script to the WanderController
  8. In the right menu, add a Node > Timer > timeout()
  9. In WanderController, add the following code:
export(int) var wander_range = 32

onready var start_position = global_position
onready var target_position = global_position

onready var timer = $Timer

func _ready():
	update_target_position()

func update_target_position():
	var target_vector = Vector2(rand_range(-wander_range, wander_range), rand_range(-wander_range, wander_range))
	target_position = start_position + target_vector

func get_time_left():
	return timer.time_left

func start_wander_timer(duration):
	timer.start(duration)

func _on_Timer_timeout():
	update_target_position()
  1. In Bat, add the following code:
onready var wanderController = $WanderController

func _ready():
	state = pick_random_state([IDLE, WANDER])

func _physics_process(delta):
	// code
	
	match state:
		IDLE:
			//code

			if wanderController.get_time_left() == 0:
				state = pick_random_state([IDLE, WANDER])
				wanderController.start_wander_timer(rand_range(1, 3))
			
		WANDER:
			seek_player()
			
			if wanderController.get_time_left() == 0:
				state = pick_random_state([IDLE, WANDER])
				wanderController.start_wander_timer(rand_range(1, 3))
			
			var direction = global_position.direction_to((wanderController.target_position))
			velocity = velocity.move_toward(direction * MAX_SPEED, ACCELERATION * delta)
                        sprite.flip_h = velocity.x < 0

// code

func pick_random_state(state_list):
	state_list.shuffle()
	return state_list.pop_front()

Prevent Wobbling Motion When Wandering

Enemies will sometimes wander, reach a position, and then seem to wobble in place for a moment. To prevent this, add the following code to Bat:

export var WANDER_TARGET_RANGE = 4

func _physics_process(delta):
	// code
	
	match state:
		//code
			
		WANDER:
			// code
			
			if global_position.distance_to(wanderController.target_position) <= WANDER_TARGET_RANGE:
				state = pick_random_state([IDLE, WANDER])
				wanderController.start_wander_timer(rand_range(1, 3))