Enemies:

To make all possible kinds of enemies that I can think of inspired by Minecraft, I used a custom Resource that describes: aspect, collisions, particles and stats. This resource is then fed to an enemy script, that can be of 4 types: melee, ranged, explosive or custom. Each class has a different attack associated:

Melee class

is simple, when it sees the target it tries to approach it and when the target is within a certain distance from the enemy, if it has a special attack associated, it will use it. An example of melee enemy is the zombie, it has no special attack. The iron golem, on the other hand, has a heavy melee attack.

Ranged class

has many parameters that describe its behaviour. In Engine these parameters are visible as a sequence of concentric circles around the enemy, these describe: max throwing distance, max idle distance and min repulsion distance. The ranged attack module is used on the skeleton, but also on the warden and the arrow dispenser and tnt dispensers.

Explosive class

works thanks to the explosive module, when the target is within the min explosion distance the enemy will stop and activate the explosive module. This will activate a state machine that counts down the time before explosion, if the player moves away the countdown increases again until it reaches the maximum explosion time.

To preview and design the different enemies I created a "mannequin" enemy, only capable, in Engine, to be dressed and have collisions placed according to the sprites.

1@tool
2extends CharacterBody2D
3
4@export var enemy_resource : EnemyResource
5@export var is_magnetic := false
6
7func _process(_delta):
8 if Engine.is_editor_hint():
9 %DeathParticles.set_deferred("process_material", enemy_resource.death_process_material)
10 %Sprite.set_deferred("sprite_frames", enemy_resource.sprite_frames)
11 %Shape.set_deferred("shape", enemy_resource.shape)
12 %HitBox/CollisionShape.set_deferred("shape", enemy_resource.hitbox_shape)
13
14 if enemy_resource.has_contact_damage:
15 %HurtBox/CollisionShape.set_deferred("shape", enemy_resource.collision_shape)
16 else:
17 %HurtBox/CollisionShape.set_deferred("shape", Rect2(0,0,1,1))
18 %HurtBox/CollisionShape.set_deferred("disabled", not enemy_resource.has_contact_damage)
19
20 var step_sensor_shape := RectangleShape2D.new()
21 step_sensor_shape.size.y = 0.3 * enemy_resource.shape.size.y
22 step_sensor_shape.size.x = 1.5 * enemy_resource.shape.size.x
23
24 %StepSensorBox/CollisionShape.set_deferred("shape", step_sensor_shape)
25 %Sprite.position.y = enemy_resource.sprite_y_offset
26
27 var shape_size_y : float = %Shape.shape.size.y / 2
28 %StepSensorBox.position.y = shape_size_y - %StepSensorBox/CollisionShape.shape.size.y / 2
29 %HurtBox.position.y = shape_size_y - %HurtBox/CollisionShape.shape.size.y / 2
30 %HitBox.position.y = shape_size_y - %HitBox/CollisionShape.shape.size.y / 2
31
32 scale.y = Math.change_direction(scale.y, -1 if is_magnetic else 1)
1class_name Math
2extends Object
3
7static func change_direction(current, direction):
8 if direction == 0: return current
9 return abs(current) * direction

Projectiles:

Projectiles use a similar modular system as the enemies. I have a custom Resource that describes the aspect and properties of the projectile, it then is attached to a specific projectile class, that can be a weighted projectile or an unweighted one.

Case study: The Warden

Receivers and Miscellaneous:

To display and design complex features with many parameters I had to create in Engine tooltips for each object of the game.

TNT:

In the simplest case I only had to display explosiveness of TNT, represented as a circle to visualize the AoE of the explosion.

Platform Manager:

In other cases I had to build custom tooltip, such as this for the Platform Manager, here I had 4 main parameters:

  • Speed of the platforms, not represented visually
  • Direction in degrees, translated into an arrow vector
  • Length of the path represented by the start and end placeholder platforms
  • Amount of platforms visualized as small equidistant dots through the direction arrow
Direction: 0°                                   Direction: 90°     
Length: 20                                      Length: 15   
Amount: 5                                       Amount: 2   
1@tool
2class_name PlatformManager
3extends Receiver
4
5@export var speed := 2.0
6
7@export_range(0, 270, 90, "degrees") var direction : float
8@export var lenght := 1.0
9@export_range(1, 100, 1) var amount := 1
10
11@export var is_active := true
12
13@export var platform_scene : PackedScene
14@export var restart_box_scene : PackedScene
15
16# Local variables and other gameplay methods
17
18func _physics_process(_delta) -> void:
19 if not Engine.is_editor_hint(): return
20 var rad_direction : float = deg_to_rad(direction)
21
22 direction_vector = Vector2.RIGHT * cos(rad_direction) + Vector2.UP * sin(rad_direction)
23
24 %MovementIndicator.target_position = direction_vector * lenght * 8
25 %EndIndicator.position = direction_vector * lenght * 8
26
27 queue_redraw()
28
29func _draw():
30 if not Engine.is_editor_hint(): return
31 var circle_offset : Vector2 = direction_vector * platform_step
32
33 platform_step = (lenght * 8) / amount
34 for i in range(amount):
35 draw_circle(%StartIndicator.position + circle_offset * i, 2, Color(Color.WHITE, 0.2))
36

Magnetic Surfaces:

Magnetic Surfaces aren't actually surfaces but instead areas that are positioned and scaled near supercharged crystal's surfaces. They are composed of an area that tells the player the magnetic force to add, and another area that detects flint and steels. To tweak the magnetic force I use 3 parameters:

  • Direction, visualized like the Platform Manager
  • Flow power, shown as the speed of the particles
  • Particles Density, this is purely aesthetical
Direction: 27°                                   Direction: 209°     
Power: 300                                       Power: 310   
Density: 0.9                                     Density: 0.2   

Camera Lerper:

To control camera movements and in general zoom and other properties I have a particular type of node for each one of them. In particular the CameraZoomLerper node is used to lerp the camera zoom from a starting zoom to an ending one based on the player's horizontal position within a certain area. This behaviour is described through 4 inputs:

  • Length and Height of the box in which the node has effect, shown through the red box
  • Starting zoom, the zoom factor at the left of the area, shown through the yellow box
  • Ending zoom, the zoom factor at the right of the area, shown through the green box
Zoom Start: 0.7                                   Zoom Start: 4.5     
Zoom End: 2.3                                     Zoom End: 0.2